summaryrefslogtreecommitdiff
path: root/android/app
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/app
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/app')
-rw-r--r--android/app/ActionBar.java1425
-rw-r--r--android/app/Activity.java7732
-rw-r--r--android/app/ActivityGroup.java130
-rw-r--r--android/app/ActivityManager.java4271
-rw-r--r--android/app/ActivityManagerInternal.java265
-rw-r--r--android/app/ActivityManagerNative.java94
-rw-r--r--android/app/ActivityOptions.java1473
-rw-r--r--android/app/ActivityThread.java6527
-rw-r--r--android/app/ActivityTransitionCoordinator.java1116
-rw-r--r--android/app/ActivityTransitionState.java381
-rw-r--r--android/app/ActivityView.java335
-rw-r--r--android/app/AlarmManager.java1125
-rw-r--r--android/app/AlertDialog.java1119
-rw-r--r--android/app/AliasActivity.java124
-rw-r--r--android/app/AppGlobals.java65
-rw-r--r--android/app/AppOpsManager.java1969
-rw-r--r--android/app/Application.java292
-rw-r--r--android/app/ApplicationErrorReport.java701
-rw-r--r--android/app/ApplicationLoaders.java125
-rw-r--r--android/app/ApplicationPackageManager.java2766
-rw-r--r--android/app/ApplicationThreadConstants.java39
-rw-r--r--android/app/AuthenticationRequiredException.java96
-rw-r--r--android/app/AutomaticZenRule.java213
-rw-r--r--android/app/BackStackRecord.java1024
-rw-r--r--android/app/BroadcastOptions.java146
-rw-r--r--android/app/ContentProviderHolder.java78
-rw-r--r--android/app/ContextImpl.java2536
-rw-r--r--android/app/DatePickerDialog.java245
-rw-r--r--android/app/DexLoadReporter.java214
-rw-r--r--android/app/Dialog.java1376
-rw-r--r--android/app/DialogFragment.java570
-rw-r--r--android/app/DownloadManager.java1633
-rw-r--r--android/app/EnterTransitionCoordinator.java731
-rw-r--r--android/app/EphemeralResolverService.java116
-rw-r--r--android/app/ExitTransitionCoordinator.java530
-rw-r--r--android/app/ExpandableListActivity.java323
-rw-r--r--android/app/Fragment.java2987
-rw-r--r--android/app/FragmentBreadCrumbs.java381
-rw-r--r--android/app/FragmentContainer.java51
-rw-r--r--android/app/FragmentController.java461
-rw-r--r--android/app/FragmentHostCallback.java388
-rw-r--r--android/app/FragmentManager.java3711
-rw-r--r--android/app/FragmentManagerNonConfig.java54
-rw-r--r--android/app/FragmentState.java137
-rw-r--r--android/app/FragmentTransaction.java402
-rw-r--r--android/app/FragmentTransition.java1386
-rw-r--r--android/app/Fragment_Delegate.java104
-rw-r--r--android/app/InstantAppResolverService.java205
-rw-r--r--android/app/Instrumentation.java2168
-rw-r--r--android/app/IntentService.java179
-rw-r--r--android/app/JobSchedulerImpl.java101
-rw-r--r--android/app/KeyguardManager.java545
-rw-r--r--android/app/LauncherActivity.java480
-rw-r--r--android/app/ListActivity.java322
-rw-r--r--android/app/ListFragment.java434
-rw-r--r--android/app/LoadedApk.java1694
-rw-r--r--android/app/LoaderManager.java902
-rw-r--r--android/app/LocalActivityManager.java633
-rw-r--r--android/app/MediaRouteActionProvider.java184
-rw-r--r--android/app/MediaRouteButton.java418
-rw-r--r--android/app/NativeActivity.java330
-rw-r--r--android/app/Notification.java8530
-rw-r--r--android/app/NotificationChannel.java853
-rw-r--r--android/app/NotificationChannelGroup.java298
-rw-r--r--android/app/NotificationManager.java1225
-rw-r--r--android/app/OnActivityPausedListener.java31
-rw-r--r--android/app/PackageDeleteObserver.java46
-rw-r--r--android/app/PackageInstallObserver.java65
-rw-r--r--android/app/PendingIntent.java1162
-rw-r--r--android/app/PictureInPictureArgs.java364
-rw-r--r--android/app/PictureInPictureParams.java283
-rw-r--r--android/app/Presentation.java369
-rw-r--r--android/app/ProfilerInfo.java135
-rw-r--r--android/app/ProgressDialog.java517
-rw-r--r--android/app/QueuedWork.java282
-rw-r--r--android/app/RecoverableSecurityException.java236
-rw-r--r--android/app/RemoteAction.java149
-rw-r--r--android/app/RemoteInput.java464
-rw-r--r--android/app/ResourcesManager.java1040
-rw-r--r--android/app/ResultInfo.java82
-rw-r--r--android/app/SearchDialog.java708
-rw-r--r--android/app/SearchManager.java993
-rw-r--r--android/app/SearchableInfo.java887
-rw-r--r--android/app/Service.java790
-rw-r--r--android/app/ServiceStartArgs.java82
-rw-r--r--android/app/SharedElementCallback.java294
-rw-r--r--android/app/SharedPreferencesImpl.java804
-rw-r--r--android/app/StatusBarManager.java240
-rw-r--r--android/app/SynchronousUserSwitchObserver.java48
-rw-r--r--android/app/SystemServiceRegistry.java1042
-rw-r--r--android/app/SystemServiceRegistry_Accessor.java29
-rw-r--r--android/app/TabActivity.java157
-rw-r--r--android/app/TaskStackBuilder.java312
-rw-r--r--android/app/TaskStackListener.java100
-rw-r--r--android/app/TimePickerDialog.java208
-rw-r--r--android/app/UiAutomation.java1187
-rw-r--r--android/app/UiAutomationConnection.java468
-rw-r--r--android/app/UiModeManager.java305
-rw-r--r--android/app/UserSwitchObserver.java41
-rw-r--r--android/app/VoiceInteractor.java1069
-rw-r--r--android/app/Vr2dDisplayProperties.java218
-rw-r--r--android/app/VrManager.java172
-rw-r--r--android/app/VrStateCallback.java38
-rw-r--r--android/app/WaitResult.java83
-rw-r--r--android/app/WallpaperColors.java426
-rw-r--r--android/app/WallpaperInfo.java388
-rw-r--r--android/app/WallpaperManager.java1976
-rw-r--r--android/app/WindowConfiguration.java515
-rw-r--r--android/app/admin/ConnectEvent.java105
-rw-r--r--android/app/admin/DeviceAdminInfo.java510
-rw-r--r--android/app/admin/DeviceAdminReceiver.java926
-rw-r--r--android/app/admin/DeviceAdminService.java65
-rw-r--r--android/app/admin/DevicePolicyManager.java8185
-rw-r--r--android/app/admin/DevicePolicyManagerInternal.java104
-rw-r--r--android/app/admin/DnsEvent.java135
-rw-r--r--android/app/admin/NetworkEvent.java98
-rw-r--r--android/app/admin/PasswordMetrics.java252
-rw-r--r--android/app/admin/SecurityLog.java247
-rw-r--r--android/app/admin/SystemUpdateInfo.java190
-rw-r--r--android/app/admin/SystemUpdatePolicy.java294
-rw-r--r--android/app/assist/AssistContent.java219
-rw-r--r--android/app/assist/AssistStructure.java2164
-rw-r--r--android/app/backup/AbsoluteFileBackupHelper.java75
-rw-r--r--android/app/backup/BackupAgent.java1143
-rw-r--r--android/app/backup/BackupAgentHelper.java97
-rw-r--r--android/app/backup/BackupDataInput.java198
-rw-r--r--android/app/backup/BackupDataInputStream.java118
-rw-r--r--android/app/backup/BackupDataOutput.java155
-rw-r--r--android/app/backup/BackupHelper.java108
-rw-r--r--android/app/backup/BackupHelperDispatcher.java148
-rw-r--r--android/app/backup/BackupManager.java755
-rw-r--r--android/app/backup/BackupManagerMonitor.java190
-rw-r--r--android/app/backup/BackupObserver.java59
-rw-r--r--android/app/backup/BackupProgress.java69
-rw-r--r--android/app/backup/BackupTransport.java707
-rw-r--r--android/app/backup/BlobBackupHelper.java318
-rw-r--r--android/app/backup/FileBackupHelper.java103
-rw-r--r--android/app/backup/FileBackupHelperBase.java127
-rw-r--r--android/app/backup/FullBackup.java665
-rw-r--r--android/app/backup/FullBackupAgent.java41
-rw-r--r--android/app/backup/FullBackupDataOutput.java56
-rw-r--r--android/app/backup/RestoreDescription.java110
-rw-r--r--android/app/backup/RestoreObserver.java79
-rw-r--r--android/app/backup/RestoreSession.java349
-rw-r--r--android/app/backup/RestoreSet.java88
-rw-r--r--android/app/backup/SelectBackupTransportCallback.java44
-rw-r--r--android/app/backup/SharedPreferencesBackupHelper.java129
-rw-r--r--android/app/backup/WallpaperBackupHelper.java109
-rw-r--r--android/app/job/JobInfo.java1189
-rw-r--r--android/app/job/JobParameters.java303
-rw-r--r--android/app/job/JobScheduler.java170
-rw-r--r--android/app/job/JobService.java137
-rw-r--r--android/app/job/JobServiceEngine.java220
-rw-r--r--android/app/job/JobWorkItem.java145
-rw-r--r--android/app/timezone/Callback.java75
-rw-r--r--android/app/timezone/DistroFormatVersion.java119
-rw-r--r--android/app/timezone/DistroRulesVersion.java131
-rw-r--r--android/app/timezone/RulesManager.java211
-rw-r--r--android/app/timezone/RulesState.java303
-rw-r--r--android/app/timezone/RulesUpdaterContract.java90
-rw-r--r--android/app/timezone/Utils.java67
-rw-r--r--android/app/trust/TrustManager.java247
-rw-r--r--android/app/usage/CacheQuotaHint.java160
-rw-r--r--android/app/usage/CacheQuotaService.java112
-rw-r--r--android/app/usage/ConfigurationStats.java161
-rw-r--r--android/app/usage/ExternalStorageStats.java145
-rw-r--r--android/app/usage/NetworkStats.java642
-rw-r--r--android/app/usage/NetworkStatsManager.java494
-rw-r--r--android/app/usage/StorageStats.java120
-rw-r--r--android/app/usage/StorageStatsManager.java340
-rw-r--r--android/app/usage/TimeSparseArray.java91
-rw-r--r--android/app/usage/UsageEvents.java506
-rw-r--r--android/app/usage/UsageStats.java255
-rw-r--r--android/app/usage/UsageStatsManager.java300
-rw-r--r--android/app/usage/UsageStatsManagerInternal.java138
175 files changed, 116678 insertions, 0 deletions
diff --git a/android/app/ActionBar.java b/android/app/ActionBar.java
new file mode 100644
index 00000000..0e8326de
--- /dev/null
+++ b/android/app/ActionBar.java
@@ -0,0 +1,1425 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.DrawableRes;
+import android.annotation.IntDef;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.Window;
+import android.widget.SpinnerAdapter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A primary toolbar within the activity that may display the activity title, application-level
+ * navigation affordances, and other interactive items.
+ *
+ * <p>Beginning with Android 3.0 (API level 11), the action bar appears at the top of an
+ * activity's window when the activity uses the system's {@link
+ * android.R.style#Theme_Holo Holo} theme (or one of its descendant themes), which is the default.
+ * You may otherwise add the action bar by calling {@link
+ * android.view.Window#requestFeature requestFeature(FEATURE_ACTION_BAR)} or by declaring it in a
+ * custom theme with the {@link android.R.styleable#Theme_windowActionBar windowActionBar} property.
+ * </p>
+ *
+ * <p>Beginning with Android L (API level 21), the action bar may be represented by any
+ * Toolbar widget within the application layout. The application may signal to the Activity
+ * which Toolbar should be treated as the Activity's action bar. Activities that use this
+ * feature should use one of the supplied <code>.NoActionBar</code> themes, set the
+ * {@link android.R.styleable#Theme_windowActionBar windowActionBar} attribute to <code>false</code>
+ * or otherwise not request the window feature.</p>
+ *
+ * <p>By adjusting the window features requested by the theme and the layouts used for
+ * an Activity's content view, an app can use the standard system action bar on older platform
+ * releases and the newer inline toolbars on newer platform releases. The <code>ActionBar</code>
+ * object obtained from the Activity can be used to control either configuration transparently.</p>
+ *
+ * <p>When using the Holo themes the action bar shows the application icon on
+ * the left, followed by the activity title. If your activity has an options menu, you can make
+ * select items accessible directly from the action bar as "action items". You can also
+ * modify various characteristics of the action bar or remove it completely.</p>
+ *
+ * <p>When using the Material themes (default in API 21 or newer) the navigation button
+ * (formerly "Home") takes over the space previously occupied by the application icon.
+ * Apps wishing to express a stronger branding should use their brand colors heavily
+ * in the action bar and other application chrome or use a {@link #setLogo(int) logo}
+ * in place of their standard title text.</p>
+ *
+ * <p>From your activity, you can retrieve an instance of {@link ActionBar} by calling {@link
+ * android.app.Activity#getActionBar getActionBar()}.</p>
+ *
+ * <p>In some cases, the action bar may be overlayed by another bar that enables contextual actions,
+ * using an {@link android.view.ActionMode}. For example, when the user selects one or more items in
+ * your activity, you can enable an action mode that offers actions specific to the selected
+ * items, with a UI that temporarily replaces the action bar. Although the UI may occupy the
+ * same space, the {@link android.view.ActionMode} APIs are distinct and independent from those for
+ * {@link ActionBar}.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about how to use the action bar, including how to add action items, navigation
+ * modes and more, read the <a href="{@docRoot}guide/topics/ui/actionbar.html">Action
+ * Bar</a> developer guide.</p>
+ * </div>
+ */
+public abstract class ActionBar {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ public @interface NavigationMode {}
+
+ /**
+ * Standard navigation mode. Consists of either a logo or icon
+ * and title text with an optional subtitle. Clicking any of these elements
+ * will dispatch onOptionsItemSelected to the host Activity with
+ * a MenuItem with item ID android.R.id.home.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public static final int NAVIGATION_MODE_STANDARD = 0;
+
+ /**
+ * List navigation mode. Instead of static title text this mode
+ * presents a list menu for navigation within the activity.
+ * e.g. this might be presented to the user as a dropdown list.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public static final int NAVIGATION_MODE_LIST = 1;
+
+ /**
+ * Tab navigation mode. Instead of static title text this mode
+ * presents a series of tabs for navigation within the activity.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public static final int NAVIGATION_MODE_TABS = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {
+ DISPLAY_USE_LOGO,
+ DISPLAY_SHOW_HOME,
+ DISPLAY_HOME_AS_UP,
+ DISPLAY_SHOW_TITLE,
+ DISPLAY_SHOW_CUSTOM,
+ DISPLAY_TITLE_MULTIPLE_LINES
+ })
+ public @interface DisplayOptions {}
+
+ /**
+ * Use logo instead of icon if available. This flag will cause appropriate
+ * navigation modes to use a wider logo in place of the standard icon.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public static final int DISPLAY_USE_LOGO = 0x1;
+
+ /**
+ * Show 'home' elements in this action bar, leaving more space for other
+ * navigation elements. This includes logo and icon.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public static final int DISPLAY_SHOW_HOME = 0x2;
+
+ /**
+ * Display the 'home' element such that it appears as an 'up' affordance.
+ * e.g. show an arrow to the left indicating the action that will be taken.
+ *
+ * Set this flag if selecting the 'home' button in the action bar to return
+ * up by a single level in your UI rather than back to the top level or front page.
+ *
+ * <p>Setting this option will implicitly enable interaction with the home/up
+ * button. See {@link #setHomeButtonEnabled(boolean)}.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public static final int DISPLAY_HOME_AS_UP = 0x4;
+
+ /**
+ * Show the activity title and subtitle, if present.
+ *
+ * @see #setTitle(CharSequence)
+ * @see #setTitle(int)
+ * @see #setSubtitle(CharSequence)
+ * @see #setSubtitle(int)
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public static final int DISPLAY_SHOW_TITLE = 0x8;
+
+ /**
+ * Show the custom view if one has been set.
+ * @see #setCustomView(View)
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public static final int DISPLAY_SHOW_CUSTOM = 0x10;
+
+ /**
+ * Allow the title to wrap onto multiple lines if space is available
+ * @hide pending API approval
+ */
+ public static final int DISPLAY_TITLE_MULTIPLE_LINES = 0x20;
+
+ /**
+ * Set the action bar into custom navigation mode, supplying a view
+ * for custom navigation.
+ *
+ * Custom navigation views appear between the application icon and
+ * any action buttons and may use any space available there. Common
+ * use cases for custom navigation views might include an auto-suggesting
+ * address bar for a browser or other navigation mechanisms that do not
+ * translate well to provided navigation modes.
+ *
+ * @param view Custom navigation view to place in the ActionBar.
+ */
+ public abstract void setCustomView(View view);
+
+ /**
+ * Set the action bar into custom navigation mode, supplying a view
+ * for custom navigation.
+ *
+ * <p>Custom navigation views appear between the application icon and
+ * any action buttons and may use any space available there. Common
+ * use cases for custom navigation views might include an auto-suggesting
+ * address bar for a browser or other navigation mechanisms that do not
+ * translate well to provided navigation modes.</p>
+ *
+ * <p>The display option {@link #DISPLAY_SHOW_CUSTOM} must be set for
+ * the custom view to be displayed.</p>
+ *
+ * @param view Custom navigation view to place in the ActionBar.
+ * @param layoutParams How this custom view should layout in the bar.
+ *
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setCustomView(View view, LayoutParams layoutParams);
+
+ /**
+ * Set the action bar into custom navigation mode, supplying a view
+ * for custom navigation.
+ *
+ * <p>Custom navigation views appear between the application icon and
+ * any action buttons and may use any space available there. Common
+ * use cases for custom navigation views might include an auto-suggesting
+ * address bar for a browser or other navigation mechanisms that do not
+ * translate well to provided navigation modes.</p>
+ *
+ * <p>The display option {@link #DISPLAY_SHOW_CUSTOM} must be set for
+ * the custom view to be displayed.</p>
+ *
+ * @param resId Resource ID of a layout to inflate into the ActionBar.
+ *
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setCustomView(@LayoutRes int resId);
+
+ /**
+ * Set the icon to display in the 'home' section of the action bar.
+ * The action bar will use an icon specified by its style or the
+ * activity icon by default.
+ *
+ * Whether the home section shows an icon or logo is controlled
+ * by the display option {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param resId Resource ID of a drawable to show as an icon.
+ *
+ * @see #setDisplayUseLogoEnabled(boolean)
+ * @see #setDisplayShowHomeEnabled(boolean)
+ */
+ public abstract void setIcon(@DrawableRes int resId);
+
+ /**
+ * Set the icon to display in the 'home' section of the action bar.
+ * The action bar will use an icon specified by its style or the
+ * activity icon by default.
+ *
+ * Whether the home section shows an icon or logo is controlled
+ * by the display option {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param icon Drawable to show as an icon.
+ *
+ * @see #setDisplayUseLogoEnabled(boolean)
+ * @see #setDisplayShowHomeEnabled(boolean)
+ */
+ public abstract void setIcon(Drawable icon);
+
+ /**
+ * Set the logo to display in the 'home' section of the action bar.
+ * The action bar will use a logo specified by its style or the
+ * activity logo by default.
+ *
+ * Whether the home section shows an icon or logo is controlled
+ * by the display option {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param resId Resource ID of a drawable to show as a logo.
+ *
+ * @see #setDisplayUseLogoEnabled(boolean)
+ * @see #setDisplayShowHomeEnabled(boolean)
+ */
+ public abstract void setLogo(@DrawableRes int resId);
+
+ /**
+ * Set the logo to display in the 'home' section of the action bar.
+ * The action bar will use a logo specified by its style or the
+ * activity logo by default.
+ *
+ * Whether the home section shows an icon or logo is controlled
+ * by the display option {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param logo Drawable to show as a logo.
+ *
+ * @see #setDisplayUseLogoEnabled(boolean)
+ * @see #setDisplayShowHomeEnabled(boolean)
+ */
+ public abstract void setLogo(Drawable logo);
+
+ /**
+ * Set the adapter and navigation callback for list navigation mode.
+ *
+ * The supplied adapter will provide views for the expanded list as well as
+ * the currently selected item. (These may be displayed differently.)
+ *
+ * The supplied OnNavigationListener will alert the application when the user
+ * changes the current list selection.
+ *
+ * @param adapter An adapter that will provide views both to display
+ * the current navigation selection and populate views
+ * within the dropdown navigation menu.
+ * @param callback An OnNavigationListener that will receive events when the user
+ * selects a navigation item.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void setListNavigationCallbacks(SpinnerAdapter adapter,
+ OnNavigationListener callback);
+
+ /**
+ * Set the selected navigation item in list or tabbed navigation modes.
+ *
+ * @param position Position of the item to select.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void setSelectedNavigationItem(int position);
+
+ /**
+ * Get the position of the selected navigation item in list or tabbed navigation modes.
+ *
+ * @return Position of the selected item.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract int getSelectedNavigationIndex();
+
+ /**
+ * Get the number of navigation items present in the current navigation mode.
+ *
+ * @return Number of navigation items.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract int getNavigationItemCount();
+
+ /**
+ * Set the action bar's title. This will only be displayed if
+ * {@link #DISPLAY_SHOW_TITLE} is set.
+ *
+ * @param title Title to set
+ *
+ * @see #setTitle(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setTitle(CharSequence title);
+
+ /**
+ * Set the action bar's title. This will only be displayed if
+ * {@link #DISPLAY_SHOW_TITLE} is set.
+ *
+ * @param resId Resource ID of title string to set
+ *
+ * @see #setTitle(CharSequence)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setTitle(@StringRes int resId);
+
+ /**
+ * Set the action bar's subtitle. This will only be displayed if
+ * {@link #DISPLAY_SHOW_TITLE} is set. Set to null to disable the
+ * subtitle entirely.
+ *
+ * @param subtitle Subtitle to set
+ *
+ * @see #setSubtitle(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setSubtitle(CharSequence subtitle);
+
+ /**
+ * Set the action bar's subtitle. This will only be displayed if
+ * {@link #DISPLAY_SHOW_TITLE} is set.
+ *
+ * @param resId Resource ID of subtitle string to set
+ *
+ * @see #setSubtitle(CharSequence)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setSubtitle(@StringRes int resId);
+
+ /**
+ * Set display options. This changes all display option bits at once. To change
+ * a limited subset of display options, see {@link #setDisplayOptions(int, int)}.
+ *
+ * @param options A combination of the bits defined by the DISPLAY_ constants
+ * defined in ActionBar.
+ */
+ public abstract void setDisplayOptions(@DisplayOptions int options);
+
+ /**
+ * Set selected display options. Only the options specified by mask will be changed.
+ * To change all display option bits at once, see {@link #setDisplayOptions(int)}.
+ *
+ * <p>Example: setDisplayOptions(0, DISPLAY_SHOW_HOME) will disable the
+ * {@link #DISPLAY_SHOW_HOME} option.
+ * setDisplayOptions(DISPLAY_SHOW_HOME, DISPLAY_SHOW_HOME | DISPLAY_USE_LOGO)
+ * will enable {@link #DISPLAY_SHOW_HOME} and disable {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param options A combination of the bits defined by the DISPLAY_ constants
+ * defined in ActionBar.
+ * @param mask A bit mask declaring which display options should be changed.
+ */
+ public abstract void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask);
+
+ /**
+ * Set whether to display the activity logo rather than the activity icon.
+ * A logo is often a wider, more detailed image.
+ *
+ * <p>To set several display options at once, see the setDisplayOptions methods.
+ *
+ * @param useLogo true to use the activity logo, false to use the activity icon.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setDisplayUseLogoEnabled(boolean useLogo);
+
+ /**
+ * Set whether to include the application home affordance in the action bar.
+ * Home is presented as either an activity icon or logo.
+ *
+ * <p>To set several display options at once, see the setDisplayOptions methods.
+ *
+ * @param showHome true to show home, false otherwise.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setDisplayShowHomeEnabled(boolean showHome);
+
+ /**
+ * Set whether home should be displayed as an "up" affordance.
+ * Set this to true if selecting "home" returns up by a single level in your UI
+ * rather than back to the top level or front page.
+ *
+ * <p>To set several display options at once, see the setDisplayOptions methods.
+ *
+ * @param showHomeAsUp true to show the user that selecting home will return one
+ * level up rather than to the top level of the app.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setDisplayHomeAsUpEnabled(boolean showHomeAsUp);
+
+ /**
+ * Set whether an activity title/subtitle should be displayed.
+ *
+ * <p>To set several display options at once, see the setDisplayOptions methods.
+ *
+ * @param showTitle true to display a title/subtitle if present.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setDisplayShowTitleEnabled(boolean showTitle);
+
+ /**
+ * Set whether a custom view should be displayed, if set.
+ *
+ * <p>To set several display options at once, see the setDisplayOptions methods.
+ *
+ * @param showCustom true if the currently set custom view should be displayed, false otherwise.
+ *
+ * @see #setDisplayOptions(int)
+ * @see #setDisplayOptions(int, int)
+ */
+ public abstract void setDisplayShowCustomEnabled(boolean showCustom);
+
+ /**
+ * Set the ActionBar's background. This will be used for the primary
+ * action bar.
+ *
+ * @param d Background drawable
+ * @see #setStackedBackgroundDrawable(Drawable)
+ * @see #setSplitBackgroundDrawable(Drawable)
+ */
+ public abstract void setBackgroundDrawable(@Nullable Drawable d);
+
+ /**
+ * Set the ActionBar's stacked background. This will appear
+ * in the second row/stacked bar on some devices and configurations.
+ *
+ * @param d Background drawable for the stacked row
+ */
+ public void setStackedBackgroundDrawable(Drawable d) { }
+
+ /**
+ * Set the ActionBar's split background. This will appear in
+ * the split action bar containing menu-provided action buttons
+ * on some devices and configurations.
+ * <p>You can enable split action bar with {@link android.R.attr#uiOptions}
+ *
+ * @param d Background drawable for the split bar
+ */
+ public void setSplitBackgroundDrawable(Drawable d) { }
+
+ /**
+ * @return The current custom view.
+ */
+ public abstract View getCustomView();
+
+ /**
+ * Returns the current ActionBar title in standard mode.
+ * Returns null if {@link #getNavigationMode()} would not return
+ * {@link #NAVIGATION_MODE_STANDARD}.
+ *
+ * @return The current ActionBar title or null.
+ */
+ public abstract CharSequence getTitle();
+
+ /**
+ * Returns the current ActionBar subtitle in standard mode.
+ * Returns null if {@link #getNavigationMode()} would not return
+ * {@link #NAVIGATION_MODE_STANDARD}.
+ *
+ * @return The current ActionBar subtitle or null.
+ */
+ public abstract CharSequence getSubtitle();
+
+ /**
+ * Returns the current navigation mode. The result will be one of:
+ * <ul>
+ * <li>{@link #NAVIGATION_MODE_STANDARD}</li>
+ * <li>{@link #NAVIGATION_MODE_LIST}</li>
+ * <li>{@link #NAVIGATION_MODE_TABS}</li>
+ * </ul>
+ *
+ * @return The current navigation mode.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ @NavigationMode
+ public abstract int getNavigationMode();
+
+ /**
+ * Set the current navigation mode.
+ *
+ * @param mode The new mode to set.
+ * @see #NAVIGATION_MODE_STANDARD
+ * @see #NAVIGATION_MODE_LIST
+ * @see #NAVIGATION_MODE_TABS
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void setNavigationMode(@NavigationMode int mode);
+
+ /**
+ * @return The current set of display options.
+ */
+ public abstract int getDisplayOptions();
+
+ /**
+ * Create and return a new {@link Tab}.
+ * This tab will not be included in the action bar until it is added.
+ *
+ * <p>Very often tabs will be used to switch between {@link Fragment}
+ * objects. Here is a typical implementation of such tabs:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentTabs.java
+ * complete}
+ *
+ * @return A new Tab
+ *
+ * @see #addTab(Tab)
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract Tab newTab();
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list.
+ * If this is the first tab to be added it will become the selected tab.
+ *
+ * @param tab Tab to add
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void addTab(Tab tab);
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list.
+ *
+ * @param tab Tab to add
+ * @param setSelected True if the added tab should become the selected tab.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void addTab(Tab tab, boolean setSelected);
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be inserted at
+ * <code>position</code>. If this is the first tab to be added it will become
+ * the selected tab.
+ *
+ * @param tab The tab to add
+ * @param position The new position of the tab
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void addTab(Tab tab, int position);
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be insterted at
+ * <code>position</code>.
+ *
+ * @param tab The tab to add
+ * @param position The new position of the tab
+ * @param setSelected True if the added tab should become the selected tab.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void addTab(Tab tab, int position, boolean setSelected);
+
+ /**
+ * Remove a tab from the action bar. If the removed tab was selected it will be deselected
+ * and another tab will be selected if present.
+ *
+ * @param tab The tab to remove
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void removeTab(Tab tab);
+
+ /**
+ * Remove a tab from the action bar. If the removed tab was selected it will be deselected
+ * and another tab will be selected if present.
+ *
+ * @param position Position of the tab to remove
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void removeTabAt(int position);
+
+ /**
+ * Remove all tabs from the action bar and deselect the current tab.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void removeAllTabs();
+
+ /**
+ * Select the specified tab. If it is not a child of this action bar it will be added.
+ *
+ * <p>Note: If you want to select by index, use {@link #setSelectedNavigationItem(int)}.</p>
+ *
+ * @param tab Tab to select
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract void selectTab(Tab tab);
+
+ /**
+ * Returns the currently selected tab if in tabbed navigation mode and there is at least
+ * one tab present.
+ *
+ * @return The currently selected tab or null
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract Tab getSelectedTab();
+
+ /**
+ * Returns the tab at the specified index.
+ *
+ * @param index Index value in the range 0-get
+ * @return
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract Tab getTabAt(int index);
+
+ /**
+ * Returns the number of tabs currently registered with the action bar.
+ * @return Tab count
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public abstract int getTabCount();
+
+ /**
+ * Retrieve the current height of the ActionBar.
+ *
+ * @return The ActionBar's height
+ */
+ public abstract int getHeight();
+
+ /**
+ * Show the ActionBar if it is not currently showing.
+ * If the window hosting the ActionBar does not have the feature
+ * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application
+ * content to fit the new space available.
+ *
+ * <p>If you are hiding the ActionBar through
+ * {@link View#SYSTEM_UI_FLAG_FULLSCREEN View.SYSTEM_UI_FLAG_FULLSCREEN},
+ * you should not call this function directly.
+ */
+ public abstract void show();
+
+ /**
+ * Hide the ActionBar if it is currently showing.
+ * If the window hosting the ActionBar does not have the feature
+ * {@link Window#FEATURE_ACTION_BAR_OVERLAY} it will resize application
+ * content to fit the new space available.
+ *
+ * <p>Instead of calling this function directly, you can also cause an
+ * ActionBar using the overlay feature to hide through
+ * {@link View#SYSTEM_UI_FLAG_FULLSCREEN View.SYSTEM_UI_FLAG_FULLSCREEN}.
+ * Hiding the ActionBar through this system UI flag allows you to more
+ * seamlessly hide it in conjunction with other screen decorations.
+ */
+ public abstract void hide();
+
+ /**
+ * @return <code>true</code> if the ActionBar is showing, <code>false</code> otherwise.
+ */
+ public abstract boolean isShowing();
+
+ /**
+ * Add a listener that will respond to menu visibility change events.
+ *
+ * @param listener The new listener to add
+ */
+ public abstract void addOnMenuVisibilityListener(OnMenuVisibilityListener listener);
+
+ /**
+ * Remove a menu visibility listener. This listener will no longer receive menu
+ * visibility change events.
+ *
+ * @param listener A listener to remove that was previously added
+ */
+ public abstract void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener);
+
+ /**
+ * Enable or disable the "home" button in the corner of the action bar. (Note that this
+ * is the application home/up affordance on the action bar, not the systemwide home
+ * button.)
+ *
+ * <p>This defaults to true for packages targeting &lt; API 14. For packages targeting
+ * API 14 or greater, the application should call this method to enable interaction
+ * with the home/up affordance.
+ *
+ * <p>Setting the {@link #DISPLAY_HOME_AS_UP} display option will automatically enable
+ * the home button.
+ *
+ * @param enabled true to enable the home button, false to disable the home button.
+ */
+ public void setHomeButtonEnabled(boolean enabled) { }
+
+ /**
+ * Returns a {@link Context} with an appropriate theme for creating views that
+ * will appear in the action bar. If you are inflating or instantiating custom views
+ * that will appear in an action bar, you should use the Context returned by this method.
+ * (This includes adapters used for list navigation mode.)
+ * This will ensure that views contrast properly against the action bar.
+ *
+ * @return A themed Context for creating views
+ */
+ public Context getThemedContext() { return null; }
+
+ /**
+ * Returns true if the Title field has been truncated during layout for lack
+ * of available space.
+ *
+ * @return true if the Title field has been truncated
+ * @hide pending API approval
+ */
+ public boolean isTitleTruncated() { return false; }
+
+ /**
+ * Set an alternate drawable to display next to the icon/logo/title
+ * when {@link #DISPLAY_HOME_AS_UP} is enabled. This can be useful if you are using
+ * this mode to display an alternate selection for up navigation, such as a sliding drawer.
+ *
+ * <p>If you pass <code>null</code> to this method, the default drawable from the theme
+ * will be used.</p>
+ *
+ * <p>If you implement alternate or intermediate behavior around Up, you should also
+ * call {@link #setHomeActionContentDescription(int) setHomeActionContentDescription()}
+ * to provide a correct description of the action for accessibility support.</p>
+ *
+ * @param indicator A drawable to use for the up indicator, or null to use the theme's default
+ *
+ * @see #setDisplayOptions(int, int)
+ * @see #setDisplayHomeAsUpEnabled(boolean)
+ * @see #setHomeActionContentDescription(int)
+ */
+ public void setHomeAsUpIndicator(Drawable indicator) { }
+
+ /**
+ * Set an alternate drawable to display next to the icon/logo/title
+ * when {@link #DISPLAY_HOME_AS_UP} is enabled. This can be useful if you are using
+ * this mode to display an alternate selection for up navigation, such as a sliding drawer.
+ *
+ * <p>If you pass <code>0</code> to this method, the default drawable from the theme
+ * will be used.</p>
+ *
+ * <p>If you implement alternate or intermediate behavior around Up, you should also
+ * call {@link #setHomeActionContentDescription(int) setHomeActionContentDescription()}
+ * to provide a correct description of the action for accessibility support.</p>
+ *
+ * @param resId Resource ID of a drawable to use for the up indicator, or null
+ * to use the theme's default
+ *
+ * @see #setDisplayOptions(int, int)
+ * @see #setDisplayHomeAsUpEnabled(boolean)
+ * @see #setHomeActionContentDescription(int)
+ */
+ public void setHomeAsUpIndicator(@DrawableRes int resId) { }
+
+ /**
+ * Set an alternate description for the Home/Up action, when enabled.
+ *
+ * <p>This description is commonly used for accessibility/screen readers when
+ * the Home action is enabled. (See {@link #setDisplayHomeAsUpEnabled(boolean)}.)
+ * Examples of this are, "Navigate Home" or "Navigate Up" depending on the
+ * {@link #DISPLAY_HOME_AS_UP} display option. If you have changed the home-as-up
+ * indicator using {@link #setHomeAsUpIndicator(int)} to indicate more specific
+ * functionality such as a sliding drawer, you should also set this to accurately
+ * describe the action.</p>
+ *
+ * <p>Setting this to <code>null</code> will use the system default description.</p>
+ *
+ * @param description New description for the Home action when enabled
+ * @see #setHomeAsUpIndicator(int)
+ * @see #setHomeAsUpIndicator(android.graphics.drawable.Drawable)
+ */
+ public void setHomeActionContentDescription(CharSequence description) { }
+
+ /**
+ * Set an alternate description for the Home/Up action, when enabled.
+ *
+ * <p>This description is commonly used for accessibility/screen readers when
+ * the Home action is enabled. (See {@link #setDisplayHomeAsUpEnabled(boolean)}.)
+ * Examples of this are, "Navigate Home" or "Navigate Up" depending on the
+ * {@link #DISPLAY_HOME_AS_UP} display option. If you have changed the home-as-up
+ * indicator using {@link #setHomeAsUpIndicator(int)} to indicate more specific
+ * functionality such as a sliding drawer, you should also set this to accurately
+ * describe the action.</p>
+ *
+ * <p>Setting this to <code>0</code> will use the system default description.</p>
+ *
+ * @param resId Resource ID of a string to use as the new description
+ * for the Home action when enabled
+ * @see #setHomeAsUpIndicator(int)
+ * @see #setHomeAsUpIndicator(android.graphics.drawable.Drawable)
+ */
+ public void setHomeActionContentDescription(@StringRes int resId) { }
+
+ /**
+ * Enable hiding the action bar on content scroll.
+ *
+ * <p>If enabled, the action bar will scroll out of sight along with a
+ * {@link View#setNestedScrollingEnabled(boolean) nested scrolling child} view's content.
+ * The action bar must be in {@link Window#FEATURE_ACTION_BAR_OVERLAY overlay mode}
+ * to enable hiding on content scroll.</p>
+ *
+ * <p>When partially scrolled off screen the action bar is considered
+ * {@link #hide() hidden}. A call to {@link #show() show} will cause it to return to full view.
+ * </p>
+ * @param hideOnContentScroll true to enable hiding on content scroll.
+ */
+ public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) {
+ if (hideOnContentScroll) {
+ throw new UnsupportedOperationException("Hide on content scroll is not supported in " +
+ "this action bar configuration.");
+ }
+ }
+
+ /**
+ * Return whether the action bar is configured to scroll out of sight along with
+ * a {@link View#setNestedScrollingEnabled(boolean) nested scrolling child}.
+ *
+ * @return true if hide-on-content-scroll is enabled
+ * @see #setHideOnContentScrollEnabled(boolean)
+ */
+ public boolean isHideOnContentScrollEnabled() {
+ return false;
+ }
+
+ /**
+ * Return the current vertical offset of the action bar.
+ *
+ * <p>The action bar's current hide offset is the distance that the action bar is currently
+ * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's
+ * current measured {@link #getHeight() height} (fully invisible).</p>
+ *
+ * @return The action bar's offset toward its fully hidden state in pixels
+ */
+ public int getHideOffset() {
+ return 0;
+ }
+
+ /**
+ * Set the current hide offset of the action bar.
+ *
+ * <p>The action bar's current hide offset is the distance that the action bar is currently
+ * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's
+ * current measured {@link #getHeight() height} (fully invisible).</p>
+ *
+ * @param offset The action bar's offset toward its fully hidden state in pixels.
+ */
+ public void setHideOffset(int offset) {
+ if (offset != 0) {
+ throw new UnsupportedOperationException("Setting an explicit action bar hide offset " +
+ "is not supported in this action bar configuration.");
+ }
+ }
+
+ /**
+ * Set the Z-axis elevation of the action bar in pixels.
+ *
+ * <p>The action bar's elevation is the distance it is placed from its parent surface. Higher
+ * values are closer to the user.</p>
+ *
+ * @param elevation Elevation value in pixels
+ */
+ public void setElevation(float elevation) {
+ if (elevation != 0) {
+ throw new UnsupportedOperationException("Setting a non-zero elevation is " +
+ "not supported in this action bar configuration.");
+ }
+ }
+
+ /**
+ * Get the Z-axis elevation of the action bar in pixels.
+ *
+ * <p>The action bar's elevation is the distance it is placed from its parent surface. Higher
+ * values are closer to the user.</p>
+ *
+ * @return Elevation value in pixels
+ */
+ public float getElevation() {
+ return 0;
+ }
+
+ /** @hide */
+ public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) {
+ }
+
+ /** @hide */
+ public void setShowHideAnimationEnabled(boolean enabled) {
+ }
+
+ /** @hide */
+ public void onConfigurationChanged(Configuration config) {
+ }
+
+ /** @hide */
+ public void dispatchMenuVisibilityChanged(boolean visible) {
+ }
+
+ /** @hide */
+ public ActionMode startActionMode(ActionMode.Callback callback) {
+ return null;
+ }
+
+ /** @hide */
+ public boolean openOptionsMenu() {
+ return false;
+ }
+
+ /** @hide */
+ public boolean closeOptionsMenu() {
+ return false;
+ }
+
+ /** @hide */
+ public boolean invalidateOptionsMenu() {
+ return false;
+ }
+
+ /** @hide */
+ public boolean onMenuKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ /** @hide */
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /** @hide */
+ public boolean collapseActionView() {
+ return false;
+ }
+
+ /** @hide */
+ public void setWindowTitle(CharSequence title) {
+ }
+
+ /** @hide */
+ public void onDestroy() {
+ }
+
+ /**
+ * Listener interface for ActionBar navigation events.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public interface OnNavigationListener {
+ /**
+ * This method is called whenever a navigation item in your action bar
+ * is selected.
+ *
+ * @param itemPosition Position of the item clicked.
+ * @param itemId ID of the item clicked.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onNavigationItemSelected(int itemPosition, long itemId);
+ }
+
+ /**
+ * Listener for receiving events when action bar menus are shown or hidden.
+ */
+ public interface OnMenuVisibilityListener {
+ /**
+ * Called when an action bar menu is shown or hidden. Applications may want to use
+ * this to tune auto-hiding behavior for the action bar or pause/resume video playback,
+ * gameplay, or other activity within the main content area.
+ *
+ * @param isVisible True if an action bar menu is now visible, false if no action bar
+ * menus are visible.
+ */
+ public void onMenuVisibilityChanged(boolean isVisible);
+ }
+
+ /**
+ * A tab in the action bar.
+ *
+ * <p>Tabs manage the hiding and showing of {@link Fragment}s.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public static abstract class Tab {
+ /**
+ * An invalid position for a tab.
+ *
+ * @see #getPosition()
+ */
+ public static final int INVALID_POSITION = -1;
+
+ /**
+ * Return the current position of this tab in the action bar.
+ *
+ * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
+ * the action bar.
+ */
+ public abstract int getPosition();
+
+ /**
+ * Return the icon associated with this tab.
+ *
+ * @return The tab's icon
+ */
+ public abstract Drawable getIcon();
+
+ /**
+ * Return the text of this tab.
+ *
+ * @return The tab's text
+ */
+ public abstract CharSequence getText();
+
+ /**
+ * Set the icon displayed on this tab.
+ *
+ * @param icon The drawable to use as an icon
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setIcon(Drawable icon);
+
+ /**
+ * Set the icon displayed on this tab.
+ *
+ * @param resId Resource ID referring to the drawable to use as an icon
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setIcon(@DrawableRes int resId);
+
+ /**
+ * Set the text displayed on this tab. Text may be truncated if there is not
+ * room to display the entire string.
+ *
+ * @param text The text to display
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setText(CharSequence text);
+
+ /**
+ * Set the text displayed on this tab. Text may be truncated if there is not
+ * room to display the entire string.
+ *
+ * @param resId A resource ID referring to the text that should be displayed
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setText(@StringRes int resId);
+
+ /**
+ * Set a custom view to be used for this tab. This overrides values set by
+ * {@link #setText(CharSequence)} and {@link #setIcon(Drawable)}.
+ *
+ * @param view Custom view to be used as a tab.
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setCustomView(View view);
+
+ /**
+ * Set a custom view to be used for this tab. This overrides values set by
+ * {@link #setText(CharSequence)} and {@link #setIcon(Drawable)}.
+ *
+ * @param layoutResId A layout resource to inflate and use as a custom tab view
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setCustomView(@LayoutRes int layoutResId);
+
+ /**
+ * Retrieve a previously set custom view for this tab.
+ *
+ * @return The custom view set by {@link #setCustomView(View)}.
+ */
+ public abstract View getCustomView();
+
+ /**
+ * Give this Tab an arbitrary object to hold for later use.
+ *
+ * @param obj Object to store
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setTag(Object obj);
+
+ /**
+ * @return This Tab's tag object.
+ */
+ public abstract Object getTag();
+
+ /**
+ * Set the {@link TabListener} that will handle switching to and from this tab.
+ * All tabs must have a TabListener set before being added to the ActionBar.
+ *
+ * @param listener Listener to handle tab selection events
+ * @return The current instance for call chaining
+ */
+ public abstract Tab setTabListener(TabListener listener);
+
+ /**
+ * Select this tab. Only valid if the tab has been added to the action bar.
+ */
+ public abstract void select();
+
+ /**
+ * Set a description of this tab's content for use in accessibility support.
+ * If no content description is provided the title will be used.
+ *
+ * @param resId A resource ID referring to the description text
+ * @return The current instance for call chaining
+ * @see #setContentDescription(CharSequence)
+ * @see #getContentDescription()
+ */
+ public abstract Tab setContentDescription(@StringRes int resId);
+
+ /**
+ * Set a description of this tab's content for use in accessibility support.
+ * If no content description is provided the title will be used.
+ *
+ * @param contentDesc Description of this tab's content
+ * @return The current instance for call chaining
+ * @see #setContentDescription(int)
+ * @see #getContentDescription()
+ */
+ public abstract Tab setContentDescription(CharSequence contentDesc);
+
+ /**
+ * Gets a brief description of this tab's content for use in accessibility support.
+ *
+ * @return Description of this tab's content
+ * @see #setContentDescription(CharSequence)
+ * @see #setContentDescription(int)
+ */
+ public abstract CharSequence getContentDescription();
+ }
+
+ /**
+ * Callback interface invoked when a tab is focused, unfocused, added, or removed.
+ *
+ * @deprecated Action bar navigation modes are deprecated and not supported by inline
+ * toolbar action bars. Consider using other
+ * <a href="http://developer.android.com/design/patterns/navigation.html">common
+ * navigation patterns</a> instead.
+ */
+ @Deprecated
+ public interface TabListener {
+ /**
+ * Called when a tab enters the selected state.
+ *
+ * @param tab The tab that was selected
+ * @param ft A {@link FragmentTransaction} for queuing fragment operations to execute
+ * during a tab switch. The previous tab's unselect and this tab's select will be
+ * executed in a single transaction. This FragmentTransaction does not support
+ * being added to the back stack.
+ */
+ public void onTabSelected(Tab tab, FragmentTransaction ft);
+
+ /**
+ * Called when a tab exits the selected state.
+ *
+ * @param tab The tab that was unselected
+ * @param ft A {@link FragmentTransaction} for queuing fragment operations to execute
+ * during a tab switch. This tab's unselect and the newly selected tab's select
+ * will be executed in a single transaction. This FragmentTransaction does not
+ * support being added to the back stack.
+ */
+ public void onTabUnselected(Tab tab, FragmentTransaction ft);
+
+ /**
+ * Called when a tab that is already selected is chosen again by the user.
+ * Some applications may use this action to return to the top level of a category.
+ *
+ * @param tab The tab that was reselected.
+ * @param ft A {@link FragmentTransaction} for queuing fragment operations to execute
+ * once this method returns. This FragmentTransaction does not support
+ * being added to the back stack.
+ */
+ public void onTabReselected(Tab tab, FragmentTransaction ft);
+ }
+
+ /**
+ * Per-child layout information associated with action bar custom views.
+ *
+ * @attr ref android.R.styleable#ActionBar_LayoutParams_layout_gravity
+ */
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ /**
+ * Gravity for the view associated with these LayoutParams.
+ *
+ * @see android.view.Gravity
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = -1, to = "NONE"),
+ @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"),
+ @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"),
+ @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"),
+ @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"),
+ @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"),
+ @ViewDebug.IntToString(from = Gravity.START, to = "START"),
+ @ViewDebug.IntToString(from = Gravity.END, to = "END"),
+ @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"),
+ @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"),
+ @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
+ @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"),
+ @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"),
+ @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL")
+ })
+ public int gravity = Gravity.NO_GRAVITY;
+
+ public LayoutParams(@NonNull Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ TypedArray a = c.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ActionBar_LayoutParams);
+ gravity = a.getInt(
+ com.android.internal.R.styleable.ActionBar_LayoutParams_layout_gravity,
+ Gravity.NO_GRAVITY);
+ a.recycle();
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ this.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
+ }
+
+ public LayoutParams(int width, int height, int gravity) {
+ super(width, height);
+
+ this.gravity = gravity;
+ }
+
+ public LayoutParams(int gravity) {
+ this(WRAP_CONTENT, MATCH_PARENT, gravity);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.gravity = source.gravity;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ /*
+ * Note for framework developers:
+ *
+ * You might notice that ActionBar.LayoutParams is missing a constructor overload
+ * for MarginLayoutParams. While it may seem like a good idea to add one, at this
+ * point it's dangerous for source compatibility. Upon building against a new
+ * version of the SDK an app can end up statically linking to the new MarginLayoutParams
+ * overload, causing a crash when running on older platform versions with no other changes.
+ */
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("gravity", gravity);
+ }
+ }
+}
diff --git a/android/app/Activity.java b/android/app/Activity.java
new file mode 100644
index 00000000..4e258a3a
--- /dev/null
+++ b/android/app/Activity.java
@@ -0,0 +1,7732 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.os.Build.VERSION_CODES.O_MR1;
+
+import static java.lang.Character.MIN_VALUE;
+
+import android.annotation.CallSuper;
+import android.annotation.DrawableRes;
+import android.annotation.IdRes;
+import android.annotation.IntDef;
+import android.annotation.LayoutRes;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.StyleRes;
+import android.annotation.SystemApi;
+import android.app.VoiceInteractor.Request;
+import android.app.admin.DevicePolicyManager;
+import android.app.assist.AssistContent;
+import android.content.ComponentCallbacks2;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.session.MediaController;
+import android.net.Uri;
+import android.os.BadParcelableException;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.StrictMode;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.method.TextKeyListener;
+import android.transition.Scene;
+import android.transition.TransitionManager;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SuperNotCalledException;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.ContextThemeWrapper;
+import android.view.DragAndDropPermissions;
+import android.view.DragEvent;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SearchEvent;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewManager;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.ActivityConfigCallback;
+import android.view.Window;
+import android.view.Window.WindowControllerCallback;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillPopupWindow;
+import android.view.autofill.IAutofillWindowPresenter;
+import android.widget.AdapterView;
+import android.widget.Toast;
+import android.widget.Toolbar;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IVoiceInteractor;
+import com.android.internal.app.ToolbarActionBar;
+import com.android.internal.app.WindowDecorActionBar;
+import com.android.internal.policy.DecorView;
+import com.android.internal.policy.PhoneWindow;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * An activity is a single, focused thing that the user can do. Almost all
+ * activities interact with the user, so the Activity class takes care of
+ * creating a window for you in which you can place your UI with
+ * {@link #setContentView}. While activities are often presented to the user
+ * as full-screen windows, they can also be used in other ways: as floating
+ * windows (via a theme with {@link android.R.attr#windowIsFloating} set)
+ * or embedded inside of another activity (using {@link ActivityGroup}).
+ *
+ * There are two methods almost all subclasses of Activity will implement:
+ *
+ * <ul>
+ * <li> {@link #onCreate} is where you initialize your activity. Most
+ * importantly, here you will usually call {@link #setContentView(int)}
+ * with a layout resource defining your UI, and using {@link #findViewById}
+ * to retrieve the widgets in that UI that you need to interact with
+ * programmatically.
+ *
+ * <li> {@link #onPause} is where you deal with the user leaving your
+ * activity. Most importantly, any changes made by the user should at this
+ * point be committed (usually to the
+ * {@link android.content.ContentProvider} holding the data).
+ * </ul>
+ *
+ * <p>To be of use with {@link android.content.Context#startActivity Context.startActivity()}, all
+ * activity classes must have a corresponding
+ * {@link android.R.styleable#AndroidManifestActivity &lt;activity&gt;}
+ * declaration in their package's <code>AndroidManifest.xml</code>.</p>
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#Fragments">Fragments</a>
+ * <li><a href="#ActivityLifecycle">Activity Lifecycle</a>
+ * <li><a href="#ConfigurationChanges">Configuration Changes</a>
+ * <li><a href="#StartingActivities">Starting Activities and Getting Results</a>
+ * <li><a href="#SavingPersistentState">Saving Persistent State</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>The Activity class is an important part of an application's overall lifecycle,
+ * and the way activities are launched and put together is a fundamental
+ * part of the platform's application model. For a detailed perspective on the structure of an
+ * Android application and how activities behave, please read the
+ * <a href="{@docRoot}guide/topics/fundamentals.html">Application Fundamentals</a> and
+ * <a href="{@docRoot}guide/components/tasks-and-back-stack.html">Tasks and Back Stack</a>
+ * developer guides.</p>
+ *
+ * <p>You can also find a detailed discussion about how to create activities in the
+ * <a href="{@docRoot}guide/components/activities.html">Activities</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * <a name="Fragments"></a>
+ * <h3>Fragments</h3>
+ *
+ * <p>Starting with {@link android.os.Build.VERSION_CODES#HONEYCOMB}, Activity
+ * implementations can make use of the {@link Fragment} class to better
+ * modularize their code, build more sophisticated user interfaces for larger
+ * screens, and help scale their application between small and large screens.
+ *
+ * <a name="ActivityLifecycle"></a>
+ * <h3>Activity Lifecycle</h3>
+ *
+ * <p>Activities in the system are managed as an <em>activity stack</em>.
+ * When a new activity is started, it is placed on the top of the stack
+ * and becomes the running activity -- the previous activity always remains
+ * below it in the stack, and will not come to the foreground again until
+ * the new activity exits.</p>
+ *
+ * <p>An activity has essentially four states:</p>
+ * <ul>
+ * <li> If an activity is in the foreground of the screen (at the top of
+ * the stack),
+ * it is <em>active</em> or <em>running</em>. </li>
+ * <li>If an activity has lost focus but is still visible (that is, a new non-full-sized
+ * or transparent activity has focus on top of your activity), it
+ * is <em>paused</em>. A paused activity is completely alive (it
+ * maintains all state and member information and remains attached to
+ * the window manager), but can be killed by the system in extreme
+ * low memory situations.
+ * <li>If an activity is completely obscured by another activity,
+ * it is <em>stopped</em>. It still retains all state and member information,
+ * however, it is no longer visible to the user so its window is hidden
+ * and it will often be killed by the system when memory is needed
+ * elsewhere.</li>
+ * <li>If an activity is paused or stopped, the system can drop the activity
+ * from memory by either asking it to finish, or simply killing its
+ * process. When it is displayed again to the user, it must be
+ * completely restarted and restored to its previous state.</li>
+ * </ul>
+ *
+ * <p>The following diagram shows the important state paths of an Activity.
+ * The square rectangles represent callback methods you can implement to
+ * perform operations when the Activity moves between states. The colored
+ * ovals are major states the Activity can be in.</p>
+ *
+ * <p><img src="../../../images/activity_lifecycle.png"
+ * alt="State diagram for an Android Activity Lifecycle." border="0" /></p>
+ *
+ * <p>There are three key loops you may be interested in monitoring within your
+ * activity:
+ *
+ * <ul>
+ * <li>The <b>entire lifetime</b> of an activity happens between the first call
+ * to {@link android.app.Activity#onCreate} through to a single final call
+ * to {@link android.app.Activity#onDestroy}. An activity will do all setup
+ * of "global" state in onCreate(), and release all remaining resources in
+ * onDestroy(). For example, if it has a thread running in the background
+ * to download data from the network, it may create that thread in onCreate()
+ * and then stop the thread in onDestroy().
+ *
+ * <li>The <b>visible lifetime</b> of an activity happens between a call to
+ * {@link android.app.Activity#onStart} until a corresponding call to
+ * {@link android.app.Activity#onStop}. During this time the user can see the
+ * activity on-screen, though it may not be in the foreground and interacting
+ * with the user. Between these two methods you can maintain resources that
+ * are needed to show the activity to the user. For example, you can register
+ * a {@link android.content.BroadcastReceiver} in onStart() to monitor for changes
+ * that impact your UI, and unregister it in onStop() when the user no
+ * longer sees what you are displaying. The onStart() and onStop() methods
+ * can be called multiple times, as the activity becomes visible and hidden
+ * to the user.
+ *
+ * <li>The <b>foreground lifetime</b> of an activity happens between a call to
+ * {@link android.app.Activity#onResume} until a corresponding call to
+ * {@link android.app.Activity#onPause}. During this time the activity is
+ * in front of all other activities and interacting with the user. An activity
+ * can frequently go between the resumed and paused states -- for example when
+ * the device goes to sleep, when an activity result is delivered, when a new
+ * intent is delivered -- so the code in these methods should be fairly
+ * lightweight.
+ * </ul>
+ *
+ * <p>The entire lifecycle of an activity is defined by the following
+ * Activity methods. All of these are hooks that you can override
+ * to do appropriate work when the activity changes state. All
+ * activities will implement {@link android.app.Activity#onCreate}
+ * to do their initial setup; many will also implement
+ * {@link android.app.Activity#onPause} to commit changes to data and
+ * otherwise prepare to stop interacting with the user. You should always
+ * call up to your superclass when implementing these methods.</p>
+ *
+ * </p>
+ * <pre class="prettyprint">
+ * public class Activity extends ApplicationContext {
+ * protected void onCreate(Bundle savedInstanceState);
+ *
+ * protected void onStart();
+ *
+ * protected void onRestart();
+ *
+ * protected void onResume();
+ *
+ * protected void onPause();
+ *
+ * protected void onStop();
+ *
+ * protected void onDestroy();
+ * }
+ * </pre>
+ *
+ * <p>In general the movement through an activity's lifecycle looks like
+ * this:</p>
+ *
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ * <colgroup align="left" span="3" />
+ * <colgroup align="left" />
+ * <colgroup align="center" />
+ * <colgroup align="center" />
+ *
+ * <thead>
+ * <tr><th colspan="3">Method</th> <th>Description</th> <th>Killable?</th> <th>Next</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><td colspan="3" align="left" border="0">{@link android.app.Activity#onCreate onCreate()}</td>
+ * <td>Called when the activity is first created.
+ * This is where you should do all of your normal static set up:
+ * create views, bind data to lists, etc. This method also
+ * provides you with a Bundle containing the activity's previously
+ * frozen state, if there was one.
+ * <p>Always followed by <code>onStart()</code>.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onStart()</code></td>
+ * </tr>
+ *
+ * <tr><td rowspan="5" style="border-left: none; border-right: none;">&nbsp;&nbsp;&nbsp;&nbsp;</td>
+ * <td colspan="2" align="left" border="0">{@link android.app.Activity#onRestart onRestart()}</td>
+ * <td>Called after your activity has been stopped, prior to it being
+ * started again.
+ * <p>Always followed by <code>onStart()</code></td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onStart()</code></td>
+ * </tr>
+ *
+ * <tr><td colspan="2" align="left" border="0">{@link android.app.Activity#onStart onStart()}</td>
+ * <td>Called when the activity is becoming visible to the user.
+ * <p>Followed by <code>onResume()</code> if the activity comes
+ * to the foreground, or <code>onStop()</code> if it becomes hidden.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onResume()</code> or <code>onStop()</code></td>
+ * </tr>
+ *
+ * <tr><td rowspan="2" style="border-left: none;">&nbsp;&nbsp;&nbsp;&nbsp;</td>
+ * <td align="left" border="0">{@link android.app.Activity#onResume onResume()}</td>
+ * <td>Called when the activity will start
+ * interacting with the user. At this point your activity is at
+ * the top of the activity stack, with user input going to it.
+ * <p>Always followed by <code>onPause()</code>.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onPause()</code></td>
+ * </tr>
+ *
+ * <tr><td align="left" border="0">{@link android.app.Activity#onPause onPause()}</td>
+ * <td>Called when the system is about to start resuming a previous
+ * activity. This is typically used to commit unsaved changes to
+ * persistent data, stop animations and other things that may be consuming
+ * CPU, etc. Implementations of this method must be very quick because
+ * the next activity will not be resumed until this method returns.
+ * <p>Followed by either <code>onResume()</code> if the activity
+ * returns back to the front, or <code>onStop()</code> if it becomes
+ * invisible to the user.</td>
+ * <td align="center"><font color="#800000"><strong>Pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB}</strong></font></td>
+ * <td align="center"><code>onResume()</code> or<br>
+ * <code>onStop()</code></td>
+ * </tr>
+ *
+ * <tr><td colspan="2" align="left" border="0">{@link android.app.Activity#onStop onStop()}</td>
+ * <td>Called when the activity is no longer visible to the user, because
+ * another activity has been resumed and is covering this one. This
+ * may happen either because a new activity is being started, an existing
+ * one is being brought in front of this one, or this one is being
+ * destroyed.
+ * <p>Followed by either <code>onRestart()</code> if
+ * this activity is coming back to interact with the user, or
+ * <code>onDestroy()</code> if this activity is going away.</td>
+ * <td align="center"><font color="#800000"><strong>Yes</strong></font></td>
+ * <td align="center"><code>onRestart()</code> or<br>
+ * <code>onDestroy()</code></td>
+ * </tr>
+ *
+ * <tr><td colspan="3" align="left" border="0">{@link android.app.Activity#onDestroy onDestroy()}</td>
+ * <td>The final call you receive before your
+ * activity is destroyed. This can happen either because the
+ * activity is finishing (someone called {@link Activity#finish} on
+ * it, or because the system is temporarily destroying this
+ * instance of the activity to save space. You can distinguish
+ * between these two scenarios with the {@link
+ * Activity#isFinishing} method.</td>
+ * <td align="center"><font color="#800000"><strong>Yes</strong></font></td>
+ * <td align="center"><em>nothing</em></td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ * <p>Note the "Killable" column in the above table -- for those methods that
+ * are marked as being killable, after that method returns the process hosting the
+ * activity may be killed by the system <em>at any time</em> without another line
+ * of its code being executed. Because of this, you should use the
+ * {@link #onPause} method to write any persistent data (such as user edits)
+ * to storage. In addition, the method
+ * {@link #onSaveInstanceState(Bundle)} is called before placing the activity
+ * in such a background state, allowing you to save away any dynamic instance
+ * state in your activity into the given Bundle, to be later received in
+ * {@link #onCreate} if the activity needs to be re-created.
+ * See the <a href="#ProcessLifecycle">Process Lifecycle</a>
+ * section for more information on how the lifecycle of a process is tied
+ * to the activities it is hosting. Note that it is important to save
+ * persistent data in {@link #onPause} instead of {@link #onSaveInstanceState}
+ * because the latter is not part of the lifecycle callbacks, so will not
+ * be called in every situation as described in its documentation.</p>
+ *
+ * <p class="note">Be aware that these semantics will change slightly between
+ * applications targeting platforms starting with {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * vs. those targeting prior platforms. Starting with Honeycomb, an application
+ * is not in the killable state until its {@link #onStop} has returned. This
+ * impacts when {@link #onSaveInstanceState(Bundle)} may be called (it may be
+ * safely called after {@link #onPause()} and allows and application to safely
+ * wait until {@link #onStop()} to save persistent state.</p>
+ *
+ * <p>For those methods that are not marked as being killable, the activity's
+ * process will not be killed by the system starting from the time the method
+ * is called and continuing after it returns. Thus an activity is in the killable
+ * state, for example, between after <code>onPause()</code> to the start of
+ * <code>onResume()</code>.</p>
+ *
+ * <a name="ConfigurationChanges"></a>
+ * <h3>Configuration Changes</h3>
+ *
+ * <p>If the configuration of the device (as defined by the
+ * {@link Configuration Resources.Configuration} class) changes,
+ * then anything displaying a user interface will need to update to match that
+ * configuration. Because Activity is the primary mechanism for interacting
+ * with the user, it includes special support for handling configuration
+ * changes.</p>
+ *
+ * <p>Unless you specify otherwise, a configuration change (such as a change
+ * in screen orientation, language, input devices, etc) will cause your
+ * current activity to be <em>destroyed</em>, going through the normal activity
+ * lifecycle process of {@link #onPause},
+ * {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity
+ * had been in the foreground or visible to the user, once {@link #onDestroy} is
+ * called in that instance then a new instance of the activity will be
+ * created, with whatever savedInstanceState the previous instance had generated
+ * from {@link #onSaveInstanceState}.</p>
+ *
+ * <p>This is done because any application resource,
+ * including layout files, can change based on any configuration value. Thus
+ * the only safe way to handle a configuration change is to re-retrieve all
+ * resources, including layouts, drawables, and strings. Because activities
+ * must already know how to save their state and re-create themselves from
+ * that state, this is a convenient way to have an activity restart itself
+ * with a new configuration.</p>
+ *
+ * <p>In some special cases, you may want to bypass restarting of your
+ * activity based on one or more types of configuration changes. This is
+ * done with the {@link android.R.attr#configChanges android:configChanges}
+ * attribute in its manifest. For any types of configuration changes you say
+ * that you handle there, you will receive a call to your current activity's
+ * {@link #onConfigurationChanged} method instead of being restarted. If
+ * a configuration change involves any that you do not handle, however, the
+ * activity will still be restarted and {@link #onConfigurationChanged}
+ * will not be called.</p>
+ *
+ * <a name="StartingActivities"></a>
+ * <h3>Starting Activities and Getting Results</h3>
+ *
+ * <p>The {@link android.app.Activity#startActivity}
+ * method is used to start a
+ * new activity, which will be placed at the top of the activity stack. It
+ * takes a single argument, an {@link android.content.Intent Intent},
+ * which describes the activity
+ * to be executed.</p>
+ *
+ * <p>Sometimes you want to get a result back from an activity when it
+ * ends. For example, you may start an activity that lets the user pick
+ * a person in a list of contacts; when it ends, it returns the person
+ * that was selected. To do this, you call the
+ * {@link android.app.Activity#startActivityForResult(Intent, int)}
+ * version with a second integer parameter identifying the call. The result
+ * will come back through your {@link android.app.Activity#onActivityResult}
+ * method.</p>
+ *
+ * <p>When an activity exits, it can call
+ * {@link android.app.Activity#setResult(int)}
+ * to return data back to its parent. It must always supply a result code,
+ * which can be the standard results RESULT_CANCELED, RESULT_OK, or any
+ * custom values starting at RESULT_FIRST_USER. In addition, it can optionally
+ * return back an Intent containing any additional data it wants. All of this
+ * information appears back on the
+ * parent's <code>Activity.onActivityResult()</code>, along with the integer
+ * identifier it originally supplied.</p>
+ *
+ * <p>If a child activity fails for any reason (such as crashing), the parent
+ * activity will receive a result with the code RESULT_CANCELED.</p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ * ...
+ *
+ * static final int PICK_CONTACT_REQUEST = 0;
+ *
+ * public boolean onKeyDown(int keyCode, KeyEvent event) {
+ * if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ * // When the user center presses, let them pick a contact.
+ * startActivityForResult(
+ * new Intent(Intent.ACTION_PICK,
+ * new Uri("content://contacts")),
+ * PICK_CONTACT_REQUEST);
+ * return true;
+ * }
+ * return false;
+ * }
+ *
+ * protected void onActivityResult(int requestCode, int resultCode,
+ * Intent data) {
+ * if (requestCode == PICK_CONTACT_REQUEST) {
+ * if (resultCode == RESULT_OK) {
+ * // A contact was picked. Here we will just display it
+ * // to the user.
+ * startActivity(new Intent(Intent.ACTION_VIEW, data));
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <a name="SavingPersistentState"></a>
+ * <h3>Saving Persistent State</h3>
+ *
+ * <p>There are generally two kinds of persistent state than an activity
+ * will deal with: shared document-like data (typically stored in a SQLite
+ * database using a {@linkplain android.content.ContentProvider content provider})
+ * and internal state such as user preferences.</p>
+ *
+ * <p>For content provider data, we suggest that activities use a
+ * "edit in place" user model. That is, any edits a user makes are effectively
+ * made immediately without requiring an additional confirmation step.
+ * Supporting this model is generally a simple matter of following two rules:</p>
+ *
+ * <ul>
+ * <li> <p>When creating a new document, the backing database entry or file for
+ * it is created immediately. For example, if the user chooses to write
+ * a new e-mail, a new entry for that e-mail is created as soon as they
+ * start entering data, so that if they go to any other activity after
+ * that point this e-mail will now appear in the list of drafts.</p>
+ * <li> <p>When an activity's <code>onPause()</code> method is called, it should
+ * commit to the backing content provider or file any changes the user
+ * has made. This ensures that those changes will be seen by any other
+ * activity that is about to run. You will probably want to commit
+ * your data even more aggressively at key times during your
+ * activity's lifecycle: for example before starting a new
+ * activity, before finishing your own activity, when the user
+ * switches between input fields, etc.</p>
+ * </ul>
+ *
+ * <p>This model is designed to prevent data loss when a user is navigating
+ * between activities, and allows the system to safely kill an activity (because
+ * system resources are needed somewhere else) at any time after it has been
+ * paused. Note this implies
+ * that the user pressing BACK from your activity does <em>not</em>
+ * mean "cancel" -- it means to leave the activity with its current contents
+ * saved away. Canceling edits in an activity must be provided through
+ * some other mechanism, such as an explicit "revert" or "undo" option.</p>
+ *
+ * <p>See the {@linkplain android.content.ContentProvider content package} for
+ * more information about content providers. These are a key aspect of how
+ * different activities invoke and propagate data between themselves.</p>
+ *
+ * <p>The Activity class also provides an API for managing internal persistent state
+ * associated with an activity. This can be used, for example, to remember
+ * the user's preferred initial display in a calendar (day view or week view)
+ * or the user's default home page in a web browser.</p>
+ *
+ * <p>Activity persistent state is managed
+ * with the method {@link #getPreferences},
+ * allowing you to retrieve and
+ * modify a set of name/value pairs associated with the activity. To use
+ * preferences that are shared across multiple application components
+ * (activities, receivers, services, providers), you can use the underlying
+ * {@link Context#getSharedPreferences Context.getSharedPreferences()} method
+ * to retrieve a preferences
+ * object stored under a specific name.
+ * (Note that it is not possible to share settings data across application
+ * packages -- for that you will need a content provider.)</p>
+ *
+ * <p>Here is an excerpt from a calendar activity that stores the user's
+ * preferred view mode in its persistent settings:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CalendarActivity extends Activity {
+ * ...
+ *
+ * static final int DAY_VIEW_MODE = 0;
+ * static final int WEEK_VIEW_MODE = 1;
+ *
+ * private SharedPreferences mPrefs;
+ * private int mCurViewMode;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * SharedPreferences mPrefs = getSharedPreferences();
+ * mCurViewMode = mPrefs.getInt("view_mode", DAY_VIEW_MODE);
+ * }
+ *
+ * protected void onPause() {
+ * super.onPause();
+ *
+ * SharedPreferences.Editor ed = mPrefs.edit();
+ * ed.putInt("view_mode", mCurViewMode);
+ * ed.commit();
+ * }
+ * }
+ * </pre>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ *
+ * <p>The ability to start a particular Activity can be enforced when it is
+ * declared in its
+ * manifest's {@link android.R.styleable#AndroidManifestActivity &lt;activity&gt;}
+ * tag. By doing so, other applications will need to declare a corresponding
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element in their own manifest to be able to start that activity.
+ *
+ * <p>When starting an Activity you can set {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} and/or {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION} on the Intent. This will grant the
+ * Activity access to the specific URIs in the Intent. Access will remain
+ * until the Activity has finished (it will remain across the hosting
+ * process being killed and other temporary destruction). As of
+ * {@link android.os.Build.VERSION_CODES#GINGERBREAD}, if the Activity
+ * was already created and a new Intent is being delivered to
+ * {@link #onNewIntent(Intent)}, any newly granted URI permissions will be added
+ * to the existing ones it holds.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/security/security.html">Security and Permissions</a>
+ * document for more information on permissions and security in general.
+ *
+ * <a name="ProcessLifecycle"></a>
+ * <h3>Process Lifecycle</h3>
+ *
+ * <p>The Android system attempts to keep application process around for as
+ * long as possible, but eventually will need to remove old processes when
+ * memory runs low. As described in <a href="#ActivityLifecycle">Activity
+ * Lifecycle</a>, the decision about which process to remove is intimately
+ * tied to the state of the user's interaction with it. In general, there
+ * are four states a process can be in based on the activities running in it,
+ * listed here in order of importance. The system will kill less important
+ * processes (the last ones) before it resorts to killing more important
+ * processes (the first ones).
+ *
+ * <ol>
+ * <li> <p>The <b>foreground activity</b> (the activity at the top of the screen
+ * that the user is currently interacting with) is considered the most important.
+ * Its process will only be killed as a last resort, if it uses more memory
+ * than is available on the device. Generally at this point the device has
+ * reached a memory paging state, so this is required in order to keep the user
+ * interface responsive.
+ * <li> <p>A <b>visible activity</b> (an activity that is visible to the user
+ * but not in the foreground, such as one sitting behind a foreground dialog)
+ * is considered extremely important and will not be killed unless that is
+ * required to keep the foreground activity running.
+ * <li> <p>A <b>background activity</b> (an activity that is not visible to
+ * the user and has been paused) is no longer critical, so the system may
+ * safely kill its process to reclaim memory for other foreground or
+ * visible processes. If its process needs to be killed, when the user navigates
+ * back to the activity (making it visible on the screen again), its
+ * {@link #onCreate} method will be called with the savedInstanceState it had previously
+ * supplied in {@link #onSaveInstanceState} so that it can restart itself in the same
+ * state as the user last left it.
+ * <li> <p>An <b>empty process</b> is one hosting no activities or other
+ * application components (such as {@link Service} or
+ * {@link android.content.BroadcastReceiver} classes). These are killed very
+ * quickly by the system as memory becomes low. For this reason, any
+ * background operation you do outside of an activity must be executed in the
+ * context of an activity BroadcastReceiver or Service to ensure that the system
+ * knows it needs to keep your process around.
+ * </ol>
+ *
+ * <p>Sometimes an Activity may need to do a long-running operation that exists
+ * independently of the activity lifecycle itself. An example may be a camera
+ * application that allows you to upload a picture to a web site. The upload
+ * may take a long time, and the application should allow the user to leave
+ * the application while it is executing. To accomplish this, your Activity
+ * should start a {@link Service} in which the upload takes place. This allows
+ * the system to properly prioritize your process (considering it to be more
+ * important than other non-visible applications) for the duration of the
+ * upload, independent of whether the original activity is paused, stopped,
+ * or finished.
+ */
+public class Activity extends ContextThemeWrapper
+ implements LayoutInflater.Factory2,
+ Window.Callback, KeyEvent.Callback,
+ OnCreateContextMenuListener, ComponentCallbacks2,
+ Window.OnWindowDismissedCallback, WindowControllerCallback,
+ AutofillManager.AutofillClient {
+ private static final String TAG = "Activity";
+ private static final boolean DEBUG_LIFECYCLE = false;
+
+ /** Standard activity result: operation canceled. */
+ public static final int RESULT_CANCELED = 0;
+ /** Standard activity result: operation succeeded. */
+ public static final int RESULT_OK = -1;
+ /** Start of user-defined activity results. */
+ public static final int RESULT_FIRST_USER = 1;
+
+ /** @hide Task isn't finished when activity is finished */
+ public static final int DONT_FINISH_TASK_WITH_ACTIVITY = 0;
+ /**
+ * @hide Task is finished if the finishing activity is the root of the task. To preserve the
+ * past behavior the task is also removed from recents.
+ */
+ public static final int FINISH_TASK_WITH_ROOT_ACTIVITY = 1;
+ /**
+ * @hide Task is finished along with the finishing activity, but it is not removed from
+ * recents.
+ */
+ public static final int FINISH_TASK_WITH_ACTIVITY = 2;
+
+ static final String FRAGMENTS_TAG = "android:fragments";
+ private static final String LAST_AUTOFILL_ID = "android:lastAutofillId";
+
+ private static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded";
+ private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";
+ private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds";
+ private static final String SAVED_DIALOGS_TAG = "android:savedDialogs";
+ private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_";
+ private static final String SAVED_DIALOG_ARGS_KEY_PREFIX = "android:dialog_args_";
+ private static final String HAS_CURENT_PERMISSIONS_REQUEST_KEY =
+ "android:hasCurrentPermissionsRequest";
+
+ private static final String REQUEST_PERMISSIONS_WHO_PREFIX = "@android:requestPermissions:";
+ private static final String AUTO_FILL_AUTH_WHO_PREFIX = "@android:autoFillAuth:";
+
+ private static final String KEYBOARD_SHORTCUTS_RECEIVER_PKG_NAME = "com.android.systemui";
+
+ private static class ManagedDialog {
+ Dialog mDialog;
+ Bundle mArgs;
+ }
+ private SparseArray<ManagedDialog> mManagedDialogs;
+
+ // set by the thread after the constructor and before onCreate(Bundle savedInstanceState) is called.
+ private Instrumentation mInstrumentation;
+ private IBinder mToken;
+ private int mIdent;
+ /*package*/ String mEmbeddedID;
+ private Application mApplication;
+ /*package*/ Intent mIntent;
+ /*package*/ String mReferrer;
+ private ComponentName mComponent;
+ /*package*/ ActivityInfo mActivityInfo;
+ /*package*/ ActivityThread mMainThread;
+ Activity mParent;
+ boolean mCalled;
+ /*package*/ boolean mResumed;
+ /*package*/ boolean mStopped;
+ boolean mFinished;
+ boolean mStartedActivity;
+ private boolean mDestroyed;
+ private boolean mDoReportFullyDrawn = true;
+ private boolean mRestoredFromBundle;
+
+ /** {@code true} if the activity lifecycle is in a state which supports picture-in-picture.
+ * This only affects the client-side exception, the actual state check still happens in AMS. */
+ private boolean mCanEnterPictureInPicture = false;
+ /** true if the activity is going through a transient pause */
+ /*package*/ boolean mTemporaryPause = false;
+ /** true if the activity is being destroyed in order to recreate it with a new configuration */
+ /*package*/ boolean mChangingConfigurations = false;
+ /*package*/ int mConfigChangeFlags;
+ /*package*/ Configuration mCurrentConfig;
+ private SearchManager mSearchManager;
+ private MenuInflater mMenuInflater;
+
+ /** The autofill manager. Always access via {@link #getAutofillManager()}. */
+ @Nullable private AutofillManager mAutofillManager;
+
+ static final class NonConfigurationInstances {
+ Object activity;
+ HashMap<String, Object> children;
+ FragmentManagerNonConfig fragments;
+ ArrayMap<String, LoaderManager> loaders;
+ VoiceInteractor voiceInteractor;
+ }
+ /* package */ NonConfigurationInstances mLastNonConfigurationInstances;
+
+ private Window mWindow;
+
+ private WindowManager mWindowManager;
+ /*package*/ View mDecor = null;
+ /*package*/ boolean mWindowAdded = false;
+ /*package*/ boolean mVisibleFromServer = false;
+ /*package*/ boolean mVisibleFromClient = true;
+ /*package*/ ActionBar mActionBar = null;
+ private boolean mEnableDefaultActionBarUp;
+
+ private VoiceInteractor mVoiceInteractor;
+
+ private CharSequence mTitle;
+ private int mTitleColor = 0;
+
+ // we must have a handler before the FragmentController is constructed
+ final Handler mHandler = new Handler();
+ final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
+
+ private static final class ManagedCursor {
+ ManagedCursor(Cursor cursor) {
+ mCursor = cursor;
+ mReleased = false;
+ mUpdated = false;
+ }
+
+ private final Cursor mCursor;
+ private boolean mReleased;
+ private boolean mUpdated;
+ }
+
+ @GuardedBy("mManagedCursors")
+ private final ArrayList<ManagedCursor> mManagedCursors = new ArrayList<>();
+
+ @GuardedBy("this")
+ int mResultCode = RESULT_CANCELED;
+ @GuardedBy("this")
+ Intent mResultData = null;
+
+ private TranslucentConversionListener mTranslucentCallback;
+ private boolean mChangeCanvasToTranslucent;
+
+ private SearchEvent mSearchEvent;
+
+ private boolean mTitleReady = false;
+ private int mActionModeTypeStarting = ActionMode.TYPE_PRIMARY;
+
+ private int mDefaultKeyMode = DEFAULT_KEYS_DISABLE;
+ private SpannableStringBuilder mDefaultKeySsb = null;
+
+ private ActivityManager.TaskDescription mTaskDescription =
+ new ActivityManager.TaskDescription();
+
+ protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused};
+
+ @SuppressWarnings("unused")
+ private final Object mInstanceTracker = StrictMode.trackActivity(this);
+
+ private Thread mUiThread;
+
+ ActivityTransitionState mActivityTransitionState = new ActivityTransitionState();
+ SharedElementCallback mEnterTransitionListener = SharedElementCallback.NULL_CALLBACK;
+ SharedElementCallback mExitTransitionListener = SharedElementCallback.NULL_CALLBACK;
+
+ private boolean mHasCurrentPermissionsRequest;
+
+ private boolean mAutoFillResetNeeded;
+
+ /** The last autofill id that was returned from {@link #getNextAutofillId()} */
+ private int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
+
+ private AutofillPopupWindow mAutofillPopupWindow;
+
+ private static native String getDlWarning();
+
+ /** Return the intent that started this activity. */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Change the intent returned by {@link #getIntent}. This holds a
+ * reference to the given intent; it does not copy it. Often used in
+ * conjunction with {@link #onNewIntent}.
+ *
+ * @param newIntent The new Intent object to return from getIntent
+ *
+ * @see #getIntent
+ * @see #onNewIntent
+ */
+ public void setIntent(Intent newIntent) {
+ mIntent = newIntent;
+ }
+
+ /** Return the application that owns this activity. */
+ public final Application getApplication() {
+ return mApplication;
+ }
+
+ /** Is this activity embedded inside of another activity? */
+ public final boolean isChild() {
+ return mParent != null;
+ }
+
+ /** Return the parent activity if this view is an embedded child. */
+ public final Activity getParent() {
+ return mParent;
+ }
+
+ /** Retrieve the window manager for showing custom windows. */
+ public WindowManager getWindowManager() {
+ return mWindowManager;
+ }
+
+ /**
+ * Retrieve the current {@link android.view.Window} for the activity.
+ * This can be used to directly access parts of the Window API that
+ * are not available through Activity/Screen.
+ *
+ * @return Window The current window, or null if the activity is not
+ * visual.
+ */
+ public Window getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Return the LoaderManager for this activity, creating it if needed.
+ */
+ public LoaderManager getLoaderManager() {
+ return mFragments.getLoaderManager();
+ }
+
+ /**
+ * Calls {@link android.view.Window#getCurrentFocus} on the
+ * Window of this Activity to return the currently focused view.
+ *
+ * @return View The current View with focus or null.
+ *
+ * @see #getWindow
+ * @see android.view.Window#getCurrentFocus
+ */
+ @Nullable
+ public View getCurrentFocus() {
+ return mWindow != null ? mWindow.getCurrentFocus() : null;
+ }
+
+ /**
+ * (Create and) return the autofill manager
+ *
+ * @return The autofill manager
+ */
+ @NonNull private AutofillManager getAutofillManager() {
+ if (mAutofillManager == null) {
+ mAutofillManager = getSystemService(AutofillManager.class);
+ }
+
+ return mAutofillManager;
+ }
+
+ /**
+ * Called when the activity is starting. This is where most initialization
+ * should go: calling {@link #setContentView(int)} to inflate the
+ * activity's UI, using {@link #findViewById} to programmatically interact
+ * with widgets in the UI, calling
+ * {@link #managedQuery(android.net.Uri , String[], String, String[], String)} to retrieve
+ * cursors for data being displayed, etc.
+ *
+ * <p>You can call {@link #finish} from within this function, in
+ * which case onDestroy() will be immediately called without any of the rest
+ * of the activity lifecycle ({@link #onStart}, {@link #onResume},
+ * {@link #onPause}, etc) executing.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @param savedInstanceState If the activity is being re-initialized after
+ * previously being shut down then this Bundle contains the data it most
+ * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b>
+ *
+ * @see #onStart
+ * @see #onSaveInstanceState
+ * @see #onRestoreInstanceState
+ * @see #onPostCreate
+ */
+ @MainThread
+ @CallSuper
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
+
+ if (getApplicationInfo().targetSdkVersion >= O_MR1 && mActivityInfo.isFixedOrientation()) {
+ final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
+ final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
+ ta.recycle();
+
+ if (isTranslucentOrFloating) {
+ throw new IllegalStateException(
+ "Only fullscreen opaque activities can request orientation");
+ }
+ }
+
+ if (mLastNonConfigurationInstances != null) {
+ mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
+ }
+ if (mActivityInfo.parentActivityName != null) {
+ if (mActionBar == null) {
+ mEnableDefaultActionBarUp = true;
+ } else {
+ mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
+ }
+ }
+ if (savedInstanceState != null) {
+ mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
+ mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID,
+ View.LAST_APP_AUTOFILL_ID);
+
+ if (mAutoFillResetNeeded) {
+ getAutofillManager().onCreate(savedInstanceState);
+ }
+
+ Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
+ mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.fragments : null);
+ }
+ mFragments.dispatchCreate();
+ getApplication().dispatchActivityCreated(this, savedInstanceState);
+ if (mVoiceInteractor != null) {
+ mVoiceInteractor.attachActivity(this);
+ }
+ mRestoredFromBundle = savedInstanceState != null;
+ mCalled = true;
+ }
+
+ /**
+ * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with
+ * the attribute {@link android.R.attr#persistableMode} set to
+ * <code>persistAcrossReboots</code>.
+ *
+ * @param savedInstanceState if the activity is being re-initialized after
+ * previously being shut down then this Bundle contains the data it most
+ * recently supplied in {@link #onSaveInstanceState}.
+ * <b><i>Note: Otherwise it is null.</i></b>
+ * @param persistentState if the activity is being re-initialized after
+ * previously being shut down or powered off then this Bundle contains the data it most
+ * recently supplied to outPersistentState in {@link #onSaveInstanceState}.
+ * <b><i>Note: Otherwise it is null.</i></b>
+ *
+ * @see #onCreate(android.os.Bundle)
+ * @see #onStart
+ * @see #onSaveInstanceState
+ * @see #onRestoreInstanceState
+ * @see #onPostCreate
+ */
+ public void onCreate(@Nullable Bundle savedInstanceState,
+ @Nullable PersistableBundle persistentState) {
+ onCreate(savedInstanceState);
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to restore the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)} and
+ * {@link #restoreManagedDialogs(android.os.Bundle)}.
+ *
+ * @param savedInstanceState contains the saved state
+ */
+ final void performRestoreInstanceState(Bundle savedInstanceState) {
+ onRestoreInstanceState(savedInstanceState);
+ restoreManagedDialogs(savedInstanceState);
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to restore the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)} and
+ * {@link #restoreManagedDialogs(android.os.Bundle)}.
+ *
+ * @param savedInstanceState contains the saved state
+ * @param persistentState contains the persistable saved state
+ */
+ final void performRestoreInstanceState(Bundle savedInstanceState,
+ PersistableBundle persistentState) {
+ onRestoreInstanceState(savedInstanceState, persistentState);
+ if (savedInstanceState != null) {
+ restoreManagedDialogs(savedInstanceState);
+ }
+ }
+
+ /**
+ * This method is called after {@link #onStart} when the activity is
+ * being re-initialized from a previously saved state, given here in
+ * <var>savedInstanceState</var>. Most implementations will simply use {@link #onCreate}
+ * to restore their state, but it is sometimes convenient to do it here
+ * after all of the initialization has been done or to allow subclasses to
+ * decide whether to use your default implementation. The default
+ * implementation of this method performs a restore of any view state that
+ * had previously been frozen by {@link #onSaveInstanceState}.
+ *
+ * <p>This method is called between {@link #onStart} and
+ * {@link #onPostCreate}.
+ *
+ * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}.
+ *
+ * @see #onCreate
+ * @see #onPostCreate
+ * @see #onResume
+ * @see #onSaveInstanceState
+ */
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (mWindow != null) {
+ Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
+ if (windowState != null) {
+ mWindow.restoreHierarchyState(windowState);
+ }
+ }
+ }
+
+ /**
+ * This is the same as {@link #onRestoreInstanceState(Bundle)} but is called for activities
+ * created with the attribute {@link android.R.attr#persistableMode} set to
+ * <code>persistAcrossReboots</code>. The {@link android.os.PersistableBundle} passed
+ * came from the restored PersistableBundle first
+ * saved in {@link #onSaveInstanceState(Bundle, PersistableBundle)}.
+ *
+ * <p>This method is called between {@link #onStart} and
+ * {@link #onPostCreate}.
+ *
+ * <p>If this method is called {@link #onRestoreInstanceState(Bundle)} will not be called.
+ *
+ * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}.
+ * @param persistentState the data most recently supplied in {@link #onSaveInstanceState}.
+ *
+ * @see #onRestoreInstanceState(Bundle)
+ * @see #onCreate
+ * @see #onPostCreate
+ * @see #onResume
+ * @see #onSaveInstanceState
+ */
+ public void onRestoreInstanceState(Bundle savedInstanceState,
+ PersistableBundle persistentState) {
+ if (savedInstanceState != null) {
+ onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ /**
+ * Restore the state of any saved managed dialogs.
+ *
+ * @param savedInstanceState The bundle to restore from.
+ */
+ private void restoreManagedDialogs(Bundle savedInstanceState) {
+ final Bundle b = savedInstanceState.getBundle(SAVED_DIALOGS_TAG);
+ if (b == null) {
+ return;
+ }
+
+ final int[] ids = b.getIntArray(SAVED_DIALOG_IDS_KEY);
+ final int numDialogs = ids.length;
+ mManagedDialogs = new SparseArray<ManagedDialog>(numDialogs);
+ for (int i = 0; i < numDialogs; i++) {
+ final Integer dialogId = ids[i];
+ Bundle dialogState = b.getBundle(savedDialogKeyFor(dialogId));
+ if (dialogState != null) {
+ // Calling onRestoreInstanceState() below will invoke dispatchOnCreate
+ // so tell createDialog() not to do it, otherwise we get an exception
+ final ManagedDialog md = new ManagedDialog();
+ md.mArgs = b.getBundle(savedDialogArgsKeyFor(dialogId));
+ md.mDialog = createDialog(dialogId, dialogState, md.mArgs);
+ if (md.mDialog != null) {
+ mManagedDialogs.put(dialogId, md);
+ onPrepareDialog(dialogId, md.mDialog, md.mArgs);
+ md.mDialog.onRestoreInstanceState(dialogState);
+ }
+ }
+ }
+ }
+
+ private Dialog createDialog(Integer dialogId, Bundle state, Bundle args) {
+ final Dialog dialog = onCreateDialog(dialogId, args);
+ if (dialog == null) {
+ return null;
+ }
+ dialog.dispatchOnCreate(state);
+ return dialog;
+ }
+
+ private static String savedDialogKeyFor(int key) {
+ return SAVED_DIALOG_KEY_PREFIX + key;
+ }
+
+ private static String savedDialogArgsKeyFor(int key) {
+ return SAVED_DIALOG_ARGS_KEY_PREFIX + key;
+ }
+
+ /**
+ * Called when activity start-up is complete (after {@link #onStart}
+ * and {@link #onRestoreInstanceState} have been called). Applications will
+ * generally not implement this method; it is intended for system
+ * classes to do final initialization after application code has run.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @param savedInstanceState If the activity is being re-initialized after
+ * previously being shut down then this Bundle contains the data it most
+ * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b>
+ * @see #onCreate
+ */
+ @CallSuper
+ protected void onPostCreate(@Nullable Bundle savedInstanceState) {
+ if (!isChild()) {
+ mTitleReady = true;
+ onTitleChanged(getTitle(), getTitleColor());
+ }
+
+ mCalled = true;
+ }
+
+ /**
+ * This is the same as {@link #onPostCreate(Bundle)} but is called for activities
+ * created with the attribute {@link android.R.attr#persistableMode} set to
+ * <code>persistAcrossReboots</code>.
+ *
+ * @param savedInstanceState The data most recently supplied in {@link #onSaveInstanceState}
+ * @param persistentState The data caming from the PersistableBundle first
+ * saved in {@link #onSaveInstanceState(Bundle, PersistableBundle)}.
+ *
+ * @see #onCreate
+ */
+ public void onPostCreate(@Nullable Bundle savedInstanceState,
+ @Nullable PersistableBundle persistentState) {
+ onPostCreate(savedInstanceState);
+ }
+
+ /**
+ * Called after {@link #onCreate} &mdash; or after {@link #onRestart} when
+ * the activity had been stopped, but is now again being displayed to the
+ * user. It will be followed by {@link #onResume}.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onCreate
+ * @see #onStop
+ * @see #onResume
+ */
+ @CallSuper
+ protected void onStart() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStart " + this);
+ mCalled = true;
+
+ mFragments.doLoaderStart();
+
+ getApplication().dispatchActivityStarted(this);
+
+ if (mAutoFillResetNeeded) {
+ AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ afm.onVisibleForAutofill();
+ }
+ }
+ }
+
+ /**
+ * Called after {@link #onStop} when the current activity is being
+ * re-displayed to the user (the user has navigated back to it). It will
+ * be followed by {@link #onStart} and then {@link #onResume}.
+ *
+ * <p>For activities that are using raw {@link Cursor} objects (instead of
+ * creating them through
+ * {@link #managedQuery(android.net.Uri , String[], String, String[], String)},
+ * this is usually the place
+ * where the cursor should be requeried (because you had deactivated it in
+ * {@link #onStop}.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onStop
+ * @see #onStart
+ * @see #onResume
+ */
+ @CallSuper
+ protected void onRestart() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when an {@link #onResume} is coming up, prior to other pre-resume callbacks
+ * such as {@link #onNewIntent} and {@link #onActivityResult}. This is primarily intended
+ * to give the activity a hint that its state is no longer saved -- it will generally
+ * be called after {@link #onSaveInstanceState} and prior to the activity being
+ * resumed/started again.
+ */
+ public void onStateNotSaved() {
+ }
+
+ /**
+ * Called after {@link #onRestoreInstanceState}, {@link #onRestart}, or
+ * {@link #onPause}, for your activity to start interacting with the user.
+ * This is a good place to begin animations, open exclusive-access devices
+ * (such as the camera), etc.
+ *
+ * <p>Keep in mind that onResume is not the best indicator that your activity
+ * is visible to the user; a system window such as the keyguard may be in
+ * front. Use {@link #onWindowFocusChanged} to know for certain that your
+ * activity is visible to the user (for example, to resume a game).
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onRestoreInstanceState
+ * @see #onRestart
+ * @see #onPostResume
+ * @see #onPause
+ */
+ @CallSuper
+ protected void onResume() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this);
+ getApplication().dispatchActivityResumed(this);
+ mActivityTransitionState.onResume(this, isTopOfTask());
+ mCalled = true;
+ }
+
+ /**
+ * Called when activity resume is complete (after {@link #onResume} has
+ * been called). Applications will generally not implement this method;
+ * it is intended for system classes to do final setup after application
+ * resume code has run.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onResume
+ */
+ @CallSuper
+ protected void onPostResume() {
+ final Window win = getWindow();
+ if (win != null) win.makeActive();
+ if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true);
+ mCalled = true;
+ }
+
+ void setVoiceInteractor(IVoiceInteractor voiceInteractor) {
+ if (mVoiceInteractor != null) {
+ for (Request activeRequest: mVoiceInteractor.getActiveRequests()) {
+ activeRequest.cancel();
+ activeRequest.clear();
+ }
+ }
+ if (voiceInteractor == null) {
+ mVoiceInteractor = null;
+ } else {
+ mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
+ Looper.myLooper());
+ }
+ }
+
+ /**
+ * Gets the next autofill ID.
+ *
+ * <p>All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned
+ * will be unique.
+ *
+ * @return A ID that is unique in the activity
+ *
+ * {@hide}
+ */
+ public int getNextAutofillId() {
+ if (mLastAutofillId == Integer.MAX_VALUE - 1) {
+ mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
+ }
+
+ mLastAutofillId++;
+
+ return mLastAutofillId;
+ }
+
+ /**
+ * Check whether this activity is running as part of a voice interaction with the user.
+ * If true, it should perform its interaction with the user through the
+ * {@link VoiceInteractor} returned by {@link #getVoiceInteractor}.
+ */
+ public boolean isVoiceInteraction() {
+ return mVoiceInteractor != null;
+ }
+
+ /**
+ * Like {@link #isVoiceInteraction}, but only returns true if this is also the root
+ * of a voice interaction. That is, returns true if this activity was directly
+ * started by the voice interaction service as the initiation of a voice interaction.
+ * Otherwise, for example if it was started by another activity while under voice
+ * interaction, returns false.
+ */
+ public boolean isVoiceInteractionRoot() {
+ try {
+ return mVoiceInteractor != null
+ && ActivityManager.getService().isRootVoiceInteraction(mToken);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the active {@link VoiceInteractor} that the user is going through to
+ * interact with this activity.
+ */
+ public VoiceInteractor getVoiceInteractor() {
+ return mVoiceInteractor;
+ }
+
+ /**
+ * Queries whether the currently enabled voice interaction service supports returning
+ * a voice interactor for use by the activity. This is valid only for the duration of the
+ * activity.
+ *
+ * @return whether the current voice interaction service supports local voice interaction
+ */
+ public boolean isLocalVoiceInteractionSupported() {
+ try {
+ return ActivityManager.getService().supportsLocalVoiceInteraction();
+ } catch (RemoteException re) {
+ }
+ return false;
+ }
+
+ /**
+ * Starts a local voice interaction session. When ready,
+ * {@link #onLocalVoiceInteractionStarted()} is called. You can pass a bundle of private options
+ * to the registered voice interaction service.
+ * @param privateOptions a Bundle of private arguments to the current voice interaction service
+ */
+ public void startLocalVoiceInteraction(Bundle privateOptions) {
+ try {
+ ActivityManager.getService().startLocalVoiceInteraction(mToken, privateOptions);
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
+ * Callback to indicate that {@link #startLocalVoiceInteraction(Bundle)} has resulted in a
+ * voice interaction session being started. You can now retrieve a voice interactor using
+ * {@link #getVoiceInteractor()}.
+ */
+ public void onLocalVoiceInteractionStarted() {
+ }
+
+ /**
+ * Callback to indicate that the local voice interaction has stopped either
+ * because it was requested through a call to {@link #stopLocalVoiceInteraction()}
+ * or because it was canceled by the user. The previously acquired {@link VoiceInteractor}
+ * is no longer valid after this.
+ */
+ public void onLocalVoiceInteractionStopped() {
+ }
+
+ /**
+ * Request to terminate the current voice interaction that was previously started
+ * using {@link #startLocalVoiceInteraction(Bundle)}. When the interaction is
+ * terminated, {@link #onLocalVoiceInteractionStopped()} will be called.
+ */
+ public void stopLocalVoiceInteraction() {
+ try {
+ ActivityManager.getService().stopLocalVoiceInteraction(mToken);
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
+ * This is called for activities that set launchMode to "singleTop" in
+ * their package, or if a client used the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP}
+ * flag when calling {@link #startActivity}. In either case, when the
+ * activity is re-launched while at the top of the activity stack instead
+ * of a new instance of the activity being started, onNewIntent() will be
+ * called on the existing instance with the Intent that was used to
+ * re-launch it.
+ *
+ * <p>An activity will always be paused before receiving a new intent, so
+ * you can count on {@link #onResume} being called after this method.
+ *
+ * <p>Note that {@link #getIntent} still returns the original Intent. You
+ * can use {@link #setIntent} to update it to this new Intent.
+ *
+ * @param intent The new intent that was started for the activity.
+ *
+ * @see #getIntent
+ * @see #setIntent
+ * @see #onResume
+ */
+ protected void onNewIntent(Intent intent) {
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to save the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)}
+ * and {@link #saveManagedDialogs(android.os.Bundle)}.
+ *
+ * @param outState The bundle to save the state to.
+ */
+ final void performSaveInstanceState(Bundle outState) {
+ onSaveInstanceState(outState);
+ saveManagedDialogs(outState);
+ mActivityTransitionState.saveState(outState);
+ storeHasCurrentPermissionRequest(outState);
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState);
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to save the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)}
+ * and {@link #saveManagedDialogs(android.os.Bundle)}.
+ *
+ * @param outState The bundle to save the state to.
+ * @param outPersistentState The bundle to save persistent state to.
+ */
+ final void performSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
+ onSaveInstanceState(outState, outPersistentState);
+ saveManagedDialogs(outState);
+ storeHasCurrentPermissionRequest(outState);
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState +
+ ", " + outPersistentState);
+ }
+
+ /**
+ * Called to retrieve per-instance state from an activity before being killed
+ * so that the state can be restored in {@link #onCreate} or
+ * {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method
+ * will be passed to both).
+ *
+ * <p>This method is called before an activity may be killed so that when it
+ * comes back some time in the future it can restore its state. For example,
+ * if activity B is launched in front of activity A, and at some point activity
+ * A is killed to reclaim resources, activity A will have a chance to save the
+ * current state of its user interface via this method so that when the user
+ * returns to activity A, the state of the user interface can be restored
+ * via {@link #onCreate} or {@link #onRestoreInstanceState}.
+ *
+ * <p>Do not confuse this method with activity lifecycle callbacks such as
+ * {@link #onPause}, which is always called when an activity is being placed
+ * in the background or on its way to destruction, or {@link #onStop} which
+ * is called before destruction. One example of when {@link #onPause} and
+ * {@link #onStop} is called and not this method is when a user navigates back
+ * from activity B to activity A: there is no need to call {@link #onSaveInstanceState}
+ * on B because that particular instance will never be restored, so the
+ * system avoids calling it. An example when {@link #onPause} is called and
+ * not {@link #onSaveInstanceState} is when activity B is launched in front of activity A:
+ * the system may avoid calling {@link #onSaveInstanceState} on activity A if it isn't
+ * killed during the lifetime of B since the state of the user interface of
+ * A will stay intact.
+ *
+ * <p>The default implementation takes care of most of the UI per-instance
+ * state for you by calling {@link android.view.View#onSaveInstanceState()} on each
+ * view in the hierarchy that has an id, and by saving the id of the currently
+ * focused view (all of which is restored by the default implementation of
+ * {@link #onRestoreInstanceState}). If you override this method to save additional
+ * information not captured by each individual view, you will likely want to
+ * call through to the default implementation, otherwise be prepared to save
+ * all of the state of each view yourself.
+ *
+ * <p>If called, this method will occur before {@link #onStop}. There are
+ * no guarantees about whether it will occur before or after {@link #onPause}.
+ *
+ * @param outState Bundle in which to place your saved state.
+ *
+ * @see #onCreate
+ * @see #onRestoreInstanceState
+ * @see #onPause
+ */
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
+
+ outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
+ Parcelable p = mFragments.saveAllState();
+ if (p != null) {
+ outState.putParcelable(FRAGMENTS_TAG, p);
+ }
+ if (mAutoFillResetNeeded) {
+ outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
+ getAutofillManager().onSaveInstanceState(outState);
+ }
+ getApplication().dispatchActivitySaveInstanceState(this, outState);
+ }
+
+ /**
+ * This is the same as {@link #onSaveInstanceState} but is called for activities
+ * created with the attribute {@link android.R.attr#persistableMode} set to
+ * <code>persistAcrossReboots</code>. The {@link android.os.PersistableBundle} passed
+ * in will be saved and presented in {@link #onCreate(Bundle, PersistableBundle)}
+ * the first time that this activity is restarted following the next device reboot.
+ *
+ * @param outState Bundle in which to place your saved state.
+ * @param outPersistentState State which will be saved across reboots.
+ *
+ * @see #onSaveInstanceState(Bundle)
+ * @see #onCreate
+ * @see #onRestoreInstanceState(Bundle, PersistableBundle)
+ * @see #onPause
+ */
+ public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
+ onSaveInstanceState(outState);
+ }
+
+ /**
+ * Save the state of any managed dialogs.
+ *
+ * @param outState place to store the saved state.
+ */
+ private void saveManagedDialogs(Bundle outState) {
+ if (mManagedDialogs == null) {
+ return;
+ }
+
+ final int numDialogs = mManagedDialogs.size();
+ if (numDialogs == 0) {
+ return;
+ }
+
+ Bundle dialogState = new Bundle();
+
+ int[] ids = new int[mManagedDialogs.size()];
+
+ // save each dialog's bundle, gather the ids
+ for (int i = 0; i < numDialogs; i++) {
+ final int key = mManagedDialogs.keyAt(i);
+ ids[i] = key;
+ final ManagedDialog md = mManagedDialogs.valueAt(i);
+ dialogState.putBundle(savedDialogKeyFor(key), md.mDialog.onSaveInstanceState());
+ if (md.mArgs != null) {
+ dialogState.putBundle(savedDialogArgsKeyFor(key), md.mArgs);
+ }
+ }
+
+ dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);
+ outState.putBundle(SAVED_DIALOGS_TAG, dialogState);
+ }
+
+
+ /**
+ * Called as part of the activity lifecycle when an activity is going into
+ * the background, but has not (yet) been killed. The counterpart to
+ * {@link #onResume}.
+ *
+ * <p>When activity B is launched in front of activity A, this callback will
+ * be invoked on A. B will not be created until A's {@link #onPause} returns,
+ * so be sure to not do anything lengthy here.
+ *
+ * <p>This callback is mostly used for saving any persistent state the
+ * activity is editing, to present a "edit in place" model to the user and
+ * making sure nothing is lost if there are not enough resources to start
+ * the new activity without first killing this one. This is also a good
+ * place to do things like stop animations and other things that consume a
+ * noticeable amount of CPU in order to make the switch to the next activity
+ * as fast as possible, or to close resources that are exclusive access
+ * such as the camera.
+ *
+ * <p>In situations where the system needs more memory it may kill paused
+ * processes to reclaim resources. Because of this, you should be sure
+ * that all of your state is saved by the time you return from
+ * this function. In general {@link #onSaveInstanceState} is used to save
+ * per-instance state in the activity and this method is used to store
+ * global persistent data (in content providers, files, etc.)
+ *
+ * <p>After receiving this call you will usually receive a following call
+ * to {@link #onStop} (after the next activity has been resumed and
+ * displayed), however in some cases there will be a direct call back to
+ * {@link #onResume} without going through the stopped state.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onResume
+ * @see #onSaveInstanceState
+ * @see #onStop
+ */
+ @CallSuper
+ protected void onPause() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this);
+ getApplication().dispatchActivityPaused(this);
+ mCalled = true;
+ }
+
+ /**
+ * Called as part of the activity lifecycle when an activity is about to go
+ * into the background as the result of user choice. For example, when the
+ * user presses the Home key, {@link #onUserLeaveHint} will be called, but
+ * when an incoming phone call causes the in-call Activity to be automatically
+ * brought to the foreground, {@link #onUserLeaveHint} will not be called on
+ * the activity being interrupted. In cases when it is invoked, this method
+ * is called right before the activity's {@link #onPause} callback.
+ *
+ * <p>This callback and {@link #onUserInteraction} are intended to help
+ * activities manage status bar notifications intelligently; specifically,
+ * for helping activities determine the proper time to cancel a notfication.
+ *
+ * @see #onUserInteraction()
+ */
+ protected void onUserLeaveHint() {
+ }
+
+ /**
+ * Generate a new thumbnail for this activity. This method is called before
+ * pausing the activity, and should draw into <var>outBitmap</var> the
+ * imagery for the desired thumbnail in the dimensions of that bitmap. It
+ * can use the given <var>canvas</var>, which is configured to draw into the
+ * bitmap, for rendering if desired.
+ *
+ * <p>The default implementation returns fails and does not draw a thumbnail;
+ * this will result in the platform creating its own thumbnail if needed.
+ *
+ * @param outBitmap The bitmap to contain the thumbnail.
+ * @param canvas Can be used to render into the bitmap.
+ *
+ * @return Return true if you have drawn into the bitmap; otherwise after
+ * you return it will be filled with a default thumbnail.
+ *
+ * @see #onCreateDescription
+ * @see #onSaveInstanceState
+ * @see #onPause
+ */
+ public boolean onCreateThumbnail(Bitmap outBitmap, Canvas canvas) {
+ return false;
+ }
+
+ /**
+ * Generate a new description for this activity. This method is called
+ * before pausing the activity and can, if desired, return some textual
+ * description of its current state to be displayed to the user.
+ *
+ * <p>The default implementation returns null, which will cause you to
+ * inherit the description from the previous activity. If all activities
+ * return null, generally the label of the top activity will be used as the
+ * description.
+ *
+ * @return A description of what the user is doing. It should be short and
+ * sweet (only a few words).
+ *
+ * @see #onCreateThumbnail
+ * @see #onSaveInstanceState
+ * @see #onPause
+ */
+ @Nullable
+ public CharSequence onCreateDescription() {
+ return null;
+ }
+
+ /**
+ * This is called when the user is requesting an assist, to build a full
+ * {@link Intent#ACTION_ASSIST} Intent with all of the context of the current
+ * application. You can override this method to place into the bundle anything
+ * you would like to appear in the {@link Intent#EXTRA_ASSIST_CONTEXT} part
+ * of the assist Intent.
+ *
+ * <p>This function will be called after any global assist callbacks that had
+ * been registered with {@link Application#registerOnProvideAssistDataListener
+ * Application.registerOnProvideAssistDataListener}.
+ */
+ public void onProvideAssistData(Bundle data) {
+ }
+
+ /**
+ * This is called when the user is requesting an assist, to provide references
+ * to content related to the current activity. Before being called, the
+ * {@code outContent} Intent is filled with the base Intent of the activity (the Intent
+ * returned by {@link #getIntent()}). The Intent's extras are stripped of any types
+ * that are not valid for {@link PersistableBundle} or non-framework Parcelables, and
+ * the flags {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} and
+ * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION} are cleared from the Intent.
+ *
+ * <p>Custom implementation may adjust the content intent to better reflect the top-level
+ * context of the activity, and fill in its ClipData with additional content of
+ * interest that the user is currently viewing. For example, an image gallery application
+ * that has launched in to an activity allowing the user to swipe through pictures should
+ * modify the intent to reference the current image they are looking it; such an
+ * application when showing a list of pictures should add a ClipData that has
+ * references to all of the pictures currently visible on screen.</p>
+ *
+ * @param outContent The assist content to return.
+ */
+ public void onProvideAssistContent(AssistContent outContent) {
+ }
+
+ /**
+ * Request the Keyboard Shortcuts screen to show up. This will trigger
+ * {@link #onProvideKeyboardShortcuts} to retrieve the shortcuts for the foreground activity.
+ */
+ public final void requestShowKeyboardShortcuts() {
+ Intent intent = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+ intent.setPackage(KEYBOARD_SHORTCUTS_RECEIVER_PKG_NAME);
+ sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ }
+
+ /**
+ * Dismiss the Keyboard Shortcuts screen.
+ */
+ public final void dismissKeyboardShortcutsHelper() {
+ Intent intent = new Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS);
+ intent.setPackage(KEYBOARD_SHORTCUTS_RECEIVER_PKG_NAME);
+ sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ }
+
+ @Override
+ public void onProvideKeyboardShortcuts(
+ List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
+ if (menu == null) {
+ return;
+ }
+ KeyboardShortcutGroup group = null;
+ int menuSize = menu.size();
+ for (int i = 0; i < menuSize; ++i) {
+ final MenuItem item = menu.getItem(i);
+ final CharSequence title = item.getTitle();
+ final char alphaShortcut = item.getAlphabeticShortcut();
+ final int alphaModifiers = item.getAlphabeticModifiers();
+ if (title != null && alphaShortcut != MIN_VALUE) {
+ if (group == null) {
+ final int resource = mApplication.getApplicationInfo().labelRes;
+ group = new KeyboardShortcutGroup(resource != 0 ? getString(resource) : null);
+ }
+ group.addItem(new KeyboardShortcutInfo(
+ title, alphaShortcut, alphaModifiers));
+ }
+ }
+ if (group != null) {
+ data.add(group);
+ }
+ }
+
+ /**
+ * Ask to have the current assistant shown to the user. This only works if the calling
+ * activity is the current foreground activity. It is the same as calling
+ * {@link android.service.voice.VoiceInteractionService#showSession
+ * VoiceInteractionService.showSession} and requesting all of the possible context.
+ * The receiver will always see
+ * {@link android.service.voice.VoiceInteractionSession#SHOW_SOURCE_APPLICATION} set.
+ * @return Returns true if the assistant was successfully invoked, else false. For example
+ * false will be returned if the caller is not the current top activity.
+ */
+ public boolean showAssist(Bundle args) {
+ try {
+ return ActivityManager.getService().showAssistFromActivity(mToken, args);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Called when you are no longer visible to the user. You will next
+ * receive either {@link #onRestart}, {@link #onDestroy}, or nothing,
+ * depending on later user activity.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onRestart
+ * @see #onResume
+ * @see #onSaveInstanceState
+ * @see #onDestroy
+ */
+ @CallSuper
+ protected void onStop() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this);
+ if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false);
+ mActivityTransitionState.onStop();
+ getApplication().dispatchActivityStopped(this);
+ mTranslucentCallback = null;
+ mCalled = true;
+
+ if (isFinishing()) {
+ if (mAutoFillResetNeeded) {
+ getAutofillManager().commit();
+ } else if (mIntent != null
+ && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
+ // Activity was launched when user tapped a link in the Autofill Save UI - since
+ // user launched another activity, the Save UI should not be restored when this
+ // activity is finished.
+ getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_CANCEL,
+ mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
+ }
+ }
+ }
+
+ /**
+ * Perform any final cleanup before an activity is destroyed. This can
+ * happen either because the activity is finishing (someone called
+ * {@link #finish} on it, or because the system is temporarily destroying
+ * this instance of the activity to save space. You can distinguish
+ * between these two scenarios with the {@link #isFinishing} method.
+ *
+ * <p><em>Note: do not count on this method being called as a place for
+ * saving data! For example, if an activity is editing data in a content
+ * provider, those edits should be committed in either {@link #onPause} or
+ * {@link #onSaveInstanceState}, not here.</em> This method is usually implemented to
+ * free resources like threads that are associated with an activity, so
+ * that a destroyed activity does not leave such things around while the
+ * rest of its application is still running. There are situations where
+ * the system will simply kill the activity's hosting process without
+ * calling this method (or any others) in it, so it should not be used to
+ * do things that are intended to remain around after the process goes
+ * away.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onPause
+ * @see #onStop
+ * @see #finish
+ * @see #isFinishing
+ */
+ @CallSuper
+ protected void onDestroy() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
+ mCalled = true;
+
+ // dismiss any dialogs we are managing.
+ if (mManagedDialogs != null) {
+ final int numDialogs = mManagedDialogs.size();
+ for (int i = 0; i < numDialogs; i++) {
+ final ManagedDialog md = mManagedDialogs.valueAt(i);
+ if (md.mDialog.isShowing()) {
+ md.mDialog.dismiss();
+ }
+ }
+ mManagedDialogs = null;
+ }
+
+ // close any cursors we are managing.
+ synchronized (mManagedCursors) {
+ int numCursors = mManagedCursors.size();
+ for (int i = 0; i < numCursors; i++) {
+ ManagedCursor c = mManagedCursors.get(i);
+ if (c != null) {
+ c.mCursor.close();
+ }
+ }
+ mManagedCursors.clear();
+ }
+
+ // Close any open search dialog
+ if (mSearchManager != null) {
+ mSearchManager.stopSearch();
+ }
+
+ if (mActionBar != null) {
+ mActionBar.onDestroy();
+ }
+
+ getApplication().dispatchActivityDestroyed(this);
+ }
+
+ /**
+ * Report to the system that your app is now fully drawn, purely for diagnostic
+ * purposes (calling it does not impact the visible behavior of the activity).
+ * This is only used to help instrument application launch times, so that the
+ * app can report when it is fully in a usable state; without this, the only thing
+ * the system itself can determine is the point at which the activity's window
+ * is <em>first</em> drawn and displayed. To participate in app launch time
+ * measurement, you should always call this method after first launch (when
+ * {@link #onCreate(android.os.Bundle)} is called), at the point where you have
+ * entirely drawn your UI and populated with all of the significant data. You
+ * can safely call this method any time after first launch as well, in which case
+ * it will simply be ignored.
+ */
+ public void reportFullyDrawn() {
+ if (mDoReportFullyDrawn) {
+ mDoReportFullyDrawn = false;
+ try {
+ ActivityManager.getService().reportActivityFullyDrawn(mToken, mRestoredFromBundle);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Called by the system when the activity changes from fullscreen mode to multi-window mode and
+ * visa-versa. This method provides the same configuration that will be sent in the following
+ * {@link #onConfigurationChanged(Configuration)} call after the activity enters this mode.
+ *
+ * @see android.R.attr#resizeableActivity
+ *
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
+ * @param newConfig The new configuration of the activity with the state
+ * {@param isInMultiWindowMode}.
+ */
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
+ // Left deliberately empty. There should be no side effects if a direct
+ // subclass of Activity does not call super.
+ onMultiWindowModeChanged(isInMultiWindowMode);
+ }
+
+ /**
+ * Called by the system when the activity changes from fullscreen mode to multi-window mode and
+ * visa-versa.
+ *
+ * @see android.R.attr#resizeableActivity
+ *
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
+ *
+ * @deprecated Use {@link #onMultiWindowModeChanged(boolean, Configuration)} instead.
+ */
+ @Deprecated
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ // Left deliberately empty. There should be no side effects if a direct
+ // subclass of Activity does not call super.
+ }
+
+ /**
+ * Returns true if the activity is currently in multi-window mode.
+ * @see android.R.attr#resizeableActivity
+ *
+ * @return True if the activity is in multi-window mode.
+ */
+ public boolean isInMultiWindowMode() {
+ try {
+ return ActivityManager.getService().isInMultiWindowMode(mToken);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Called by the system when the activity changes to and from picture-in-picture mode. This
+ * method provides the same configuration that will be sent in the following
+ * {@link #onConfigurationChanged(Configuration)} call after the activity enters this mode.
+ *
+ * @see android.R.attr#supportsPictureInPicture
+ *
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
+ * @param newConfig The new configuration of the activity with the state
+ * {@param isInPictureInPictureMode}.
+ */
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ // Left deliberately empty. There should be no side effects if a direct
+ // subclass of Activity does not call super.
+ onPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+
+ /**
+ * Called by the system when the activity changes to and from picture-in-picture mode.
+ *
+ * @see android.R.attr#supportsPictureInPicture
+ *
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
+ *
+ * @deprecated Use {@link #onPictureInPictureModeChanged(boolean, Configuration)} instead.
+ */
+ @Deprecated
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ // Left deliberately empty. There should be no side effects if a direct
+ // subclass of Activity does not call super.
+ }
+
+ /**
+ * Returns true if the activity is currently in picture-in-picture mode.
+ * @see android.R.attr#supportsPictureInPicture
+ *
+ * @return True if the activity is in picture-in-picture mode.
+ */
+ public boolean isInPictureInPictureMode() {
+ try {
+ return ActivityManager.getService().isInPictureInPictureMode(mToken);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Puts the activity in picture-in-picture mode if possible in the current system state. Any
+ * prior calls to {@link #setPictureInPictureParams(PictureInPictureParams)} will still apply
+ * when entering picture-in-picture through this call.
+ *
+ * @see #enterPictureInPictureMode(PictureInPictureParams)
+ * @see android.R.attr#supportsPictureInPicture
+ */
+ @Deprecated
+ public void enterPictureInPictureMode() {
+ enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
+ }
+
+ /** @removed */
+ @Deprecated
+ public boolean enterPictureInPictureMode(@NonNull PictureInPictureArgs args) {
+ return enterPictureInPictureMode(PictureInPictureArgs.convert(args));
+ }
+
+ /**
+ * Puts the activity in picture-in-picture mode if possible in the current system state. The
+ * set parameters in {@param params} will be combined with the parameters from prior calls to
+ * {@link #setPictureInPictureParams(PictureInPictureParams)}.
+ *
+ * The system may disallow entering picture-in-picture in various cases, including when the
+ * activity is not visible, if the screen is locked or if the user has an activity pinned.
+ *
+ * @see android.R.attr#supportsPictureInPicture
+ * @see PictureInPictureParams
+ *
+ * @param params non-null parameters to be combined with previously set parameters when entering
+ * picture-in-picture.
+ *
+ * @return true if the system puts this activity into picture-in-picture mode or was already
+ * in picture-in-picture mode (@see {@link #isInPictureInPictureMode())
+ */
+ public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) {
+ try {
+ if (params == null) {
+ throw new IllegalArgumentException("Expected non-null picture-in-picture params");
+ }
+ if (!mCanEnterPictureInPicture) {
+ throw new IllegalStateException("Activity must be resumed to enter"
+ + " picture-in-picture");
+ }
+ return ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken, params);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public void setPictureInPictureArgs(@NonNull PictureInPictureArgs args) {
+ setPictureInPictureParams(PictureInPictureArgs.convert(args));
+ }
+
+ /**
+ * Updates the properties of the picture-in-picture activity, or sets it to be used later when
+ * {@link #enterPictureInPictureMode()} is called.
+ *
+ * @param params the new parameters for the picture-in-picture.
+ */
+ public void setPictureInPictureParams(@NonNull PictureInPictureParams params) {
+ try {
+ if (params == null) {
+ throw new IllegalArgumentException("Expected non-null picture-in-picture params");
+ }
+ ActivityManagerNative.getDefault().setPictureInPictureParams(mToken, params);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Return the number of actions that will be displayed in the picture-in-picture UI when the
+ * user interacts with the activity currently in picture-in-picture mode. This number may change
+ * if the global configuration changes (ie. if the device is plugged into an external display),
+ * but will always be larger than three.
+ */
+ public int getMaxNumPictureInPictureActions() {
+ try {
+ return ActivityManagerNative.getDefault().getMaxNumPictureInPictureActions(mToken);
+ } catch (RemoteException e) {
+ return 0;
+ }
+ }
+
+ void dispatchMovedToDisplay(int displayId, Configuration config) {
+ updateDisplay(displayId);
+ onMovedToDisplay(displayId, config);
+ }
+
+ /**
+ * Called by the system when the activity is moved from one display to another without
+ * recreation. This means that this activity is declared to handle all changes to configuration
+ * that happened when it was switched to another display, so it wasn't destroyed and created
+ * again.
+ *
+ * <p>This call will be followed by {@link #onConfigurationChanged(Configuration)} if the
+ * applied configuration actually changed. It is up to app developer to choose whether to handle
+ * the change in this method or in the following {@link #onConfigurationChanged(Configuration)}
+ * call.
+ *
+ * <p>Use this callback to track changes to the displays if some activity functionality relies
+ * on an association with some display properties.
+ *
+ * @param displayId The id of the display to which activity was moved.
+ * @param config Configuration of the activity resources on new display after move.
+ *
+ * @see #onConfigurationChanged(Configuration)
+ * @see View#onMovedToDisplay(int, Configuration)
+ * @hide
+ */
+ public void onMovedToDisplay(int displayId, Configuration config) {
+ }
+
+ /**
+ * Called by the system when the device configuration changes while your
+ * activity is running. Note that this will <em>only</em> be called if
+ * you have selected configurations you would like to handle with the
+ * {@link android.R.attr#configChanges} attribute in your manifest. If
+ * any configuration change occurs that is not selected to be reported
+ * by that attribute, then instead of reporting it the system will stop
+ * and restart the activity (to have it launched with the new
+ * configuration).
+ *
+ * <p>At the time that this function has been called, your Resources
+ * object will have been updated to return resource values matching the
+ * new configuration.
+ *
+ * @param newConfig The new device configuration.
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onConfigurationChanged " + this + ": " + newConfig);
+ mCalled = true;
+
+ mFragments.dispatchConfigurationChanged(newConfig);
+
+ if (mWindow != null) {
+ // Pass the configuration changed event to the window
+ mWindow.onConfigurationChanged(newConfig);
+ }
+
+ if (mActionBar != null) {
+ // Do this last; the action bar will need to access
+ // view changes from above.
+ mActionBar.onConfigurationChanged(newConfig);
+ }
+ }
+
+ /**
+ * If this activity is being destroyed because it can not handle a
+ * configuration parameter being changed (and thus its
+ * {@link #onConfigurationChanged(Configuration)} method is
+ * <em>not</em> being called), then you can use this method to discover
+ * the set of changes that have occurred while in the process of being
+ * destroyed. Note that there is no guarantee that these will be
+ * accurate (other changes could have happened at any time), so you should
+ * only use this as an optimization hint.
+ *
+ * @return Returns a bit field of the configuration parameters that are
+ * changing, as defined by the {@link android.content.res.Configuration}
+ * class.
+ */
+ public int getChangingConfigurations() {
+ return mConfigChangeFlags;
+ }
+
+ /**
+ * Retrieve the non-configuration instance data that was previously
+ * returned by {@link #onRetainNonConfigurationInstance()}. This will
+ * be available from the initial {@link #onCreate} and
+ * {@link #onStart} calls to the new instance, allowing you to extract
+ * any useful dynamic state from the previous instance.
+ *
+ * <p>Note that the data you retrieve here should <em>only</em> be used
+ * as an optimization for handling configuration changes. You should always
+ * be able to handle getting a null pointer back, and an activity must
+ * still be able to restore itself to its previous state (through the
+ * normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this
+ * function returns null.
+ *
+ * <p><strong>Note:</strong> For most cases you should use the {@link Fragment} API
+ * {@link Fragment#setRetainInstance(boolean)} instead; this is also
+ * available on older platforms through the Android support libraries.
+ *
+ * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
+ */
+ @Nullable
+ public Object getLastNonConfigurationInstance() {
+ return mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.activity : null;
+ }
+
+ /**
+ * Called by the system, as part of destroying an
+ * activity due to a configuration change, when it is known that a new
+ * instance will immediately be created for the new configuration. You
+ * can return any object you like here, including the activity instance
+ * itself, which can later be retrieved by calling
+ * {@link #getLastNonConfigurationInstance()} in the new activity
+ * instance.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using a {@link Fragment} with
+ * {@link Fragment#setRetainInstance(boolean)
+ * Fragment.setRetainInstance(boolean}.</em>
+ *
+ * <p>This function is called purely as an optimization, and you must
+ * not rely on it being called. When it is called, a number of guarantees
+ * will be made to help optimize configuration switching:
+ * <ul>
+ * <li> The function will be called between {@link #onStop} and
+ * {@link #onDestroy}.
+ * <li> A new instance of the activity will <em>always</em> be immediately
+ * created after this one's {@link #onDestroy()} is called. In particular,
+ * <em>no</em> messages will be dispatched during this time (when the returned
+ * object does not have an activity to be associated with).
+ * <li> The object you return here will <em>always</em> be available from
+ * the {@link #getLastNonConfigurationInstance()} method of the following
+ * activity instance as described there.
+ * </ul>
+ *
+ * <p>These guarantees are designed so that an activity can use this API
+ * to propagate extensive state from the old to new activity instance, from
+ * loaded bitmaps, to network connections, to evenly actively running
+ * threads. Note that you should <em>not</em> propagate any data that
+ * may change based on the configuration, including any data loaded from
+ * resources such as strings, layouts, or drawables.
+ *
+ * <p>The guarantee of no message handling during the switch to the next
+ * activity simplifies use with active objects. For example if your retained
+ * state is an {@link android.os.AsyncTask} you are guaranteed that its
+ * call back functions (like {@link android.os.AsyncTask#onPostExecute}) will
+ * not be called from the call here until you execute the next instance's
+ * {@link #onCreate(Bundle)}. (Note however that there is of course no such
+ * guarantee for {@link android.os.AsyncTask#doInBackground} since that is
+ * running in a separate thread.)
+ *
+ * <p><strong>Note:</strong> For most cases you should use the {@link Fragment} API
+ * {@link Fragment#setRetainInstance(boolean)} instead; this is also
+ * available on older platforms through the Android support libraries.
+ *
+ * @return any Object holding the desired state to propagate to the
+ * next activity instance
+ */
+ public Object onRetainNonConfigurationInstance() {
+ return null;
+ }
+
+ /**
+ * Retrieve the non-configuration instance data that was previously
+ * returned by {@link #onRetainNonConfigurationChildInstances()}. This will
+ * be available from the initial {@link #onCreate} and
+ * {@link #onStart} calls to the new instance, allowing you to extract
+ * any useful dynamic state from the previous instance.
+ *
+ * <p>Note that the data you retrieve here should <em>only</em> be used
+ * as an optimization for handling configuration changes. You should always
+ * be able to handle getting a null pointer back, and an activity must
+ * still be able to restore itself to its previous state (through the
+ * normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this
+ * function returns null.
+ *
+ * @return Returns the object previously returned by
+ * {@link #onRetainNonConfigurationChildInstances()}
+ */
+ @Nullable
+ HashMap<String, Object> getLastNonConfigurationChildInstances() {
+ return mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.children : null;
+ }
+
+ /**
+ * This method is similar to {@link #onRetainNonConfigurationInstance()} except that
+ * it should return either a mapping from child activity id strings to arbitrary objects,
+ * or null. This method is intended to be used by Activity framework subclasses that control a
+ * set of child activities, such as ActivityGroup. The same guarantees and restrictions apply
+ * as for {@link #onRetainNonConfigurationInstance()}. The default implementation returns null.
+ */
+ @Nullable
+ HashMap<String,Object> onRetainNonConfigurationChildInstances() {
+ return null;
+ }
+
+ NonConfigurationInstances retainNonConfigurationInstances() {
+ Object activity = onRetainNonConfigurationInstance();
+ HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
+ FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
+
+ // We're already stopped but we've been asked to retain.
+ // Our fragments are taken care of but we need to mark the loaders for retention.
+ // In order to do this correctly we need to restart the loaders first before
+ // handing them off to the next activity.
+ mFragments.doLoaderStart();
+ mFragments.doLoaderStop(true);
+ ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();
+
+ if (activity == null && children == null && fragments == null && loaders == null
+ && mVoiceInteractor == null) {
+ return null;
+ }
+
+ NonConfigurationInstances nci = new NonConfigurationInstances();
+ nci.activity = activity;
+ nci.children = children;
+ nci.fragments = fragments;
+ nci.loaders = loaders;
+ if (mVoiceInteractor != null) {
+ mVoiceInteractor.retainInstance();
+ nci.voiceInteractor = mVoiceInteractor;
+ }
+ return nci;
+ }
+
+ public void onLowMemory() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onLowMemory " + this);
+ mCalled = true;
+ mFragments.dispatchLowMemory();
+ }
+
+ public void onTrimMemory(int level) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onTrimMemory " + this + ": " + level);
+ mCalled = true;
+ mFragments.dispatchTrimMemory(level);
+ }
+
+ /**
+ * Return the FragmentManager for interacting with fragments associated
+ * with this activity.
+ */
+ public FragmentManager getFragmentManager() {
+ return mFragments.getFragmentManager();
+ }
+
+ /**
+ * Called when a Fragment is being attached to this activity, immediately
+ * after the call to its {@link Fragment#onAttach Fragment.onAttach()}
+ * method and before {@link Fragment#onCreate Fragment.onCreate()}.
+ */
+ public void onAttachFragment(Fragment fragment) {
+ }
+
+ /**
+ * Wrapper around
+ * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)}
+ * that gives the resulting {@link Cursor} to call
+ * {@link #startManagingCursor} so that the activity will manage its
+ * lifecycle for you.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using {@link LoaderManager} instead, available
+ * via {@link #getLoaderManager()}.</em>
+ *
+ * <p><strong>Warning:</strong> Do not call {@link Cursor#close()} on a cursor obtained using
+ * this method, because the activity will do that for you at the appropriate time. However, if
+ * you call {@link #stopManagingCursor} on a cursor from a managed query, the system <em>will
+ * not</em> automatically close the cursor and, in that case, you must call
+ * {@link Cursor#close()}.</p>
+ *
+ * @param uri The URI of the content provider to query.
+ * @param projection List of columns to return.
+ * @param selection SQL WHERE clause.
+ * @param sortOrder SQL ORDER BY clause.
+ *
+ * @return The Cursor that was returned by query().
+ *
+ * @see ContentResolver#query(android.net.Uri , String[], String, String[], String)
+ * @see #startManagingCursor
+ * @hide
+ *
+ * @deprecated Use {@link CursorLoader} instead.
+ */
+ @Deprecated
+ public final Cursor managedQuery(Uri uri, String[] projection, String selection,
+ String sortOrder) {
+ Cursor c = getContentResolver().query(uri, projection, selection, null, sortOrder);
+ if (c != null) {
+ startManagingCursor(c);
+ }
+ return c;
+ }
+
+ /**
+ * Wrapper around
+ * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)}
+ * that gives the resulting {@link Cursor} to call
+ * {@link #startManagingCursor} so that the activity will manage its
+ * lifecycle for you.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using {@link LoaderManager} instead, available
+ * via {@link #getLoaderManager()}.</em>
+ *
+ * <p><strong>Warning:</strong> Do not call {@link Cursor#close()} on a cursor obtained using
+ * this method, because the activity will do that for you at the appropriate time. However, if
+ * you call {@link #stopManagingCursor} on a cursor from a managed query, the system <em>will
+ * not</em> automatically close the cursor and, in that case, you must call
+ * {@link Cursor#close()}.</p>
+ *
+ * @param uri The URI of the content provider to query.
+ * @param projection List of columns to return.
+ * @param selection SQL WHERE clause.
+ * @param selectionArgs The arguments to selection, if any ?s are pesent
+ * @param sortOrder SQL ORDER BY clause.
+ *
+ * @return The Cursor that was returned by query().
+ *
+ * @see ContentResolver#query(android.net.Uri , String[], String, String[], String)
+ * @see #startManagingCursor
+ *
+ * @deprecated Use {@link CursorLoader} instead.
+ */
+ @Deprecated
+ public final Cursor managedQuery(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
+ if (c != null) {
+ startManagingCursor(c);
+ }
+ return c;
+ }
+
+ /**
+ * This method allows the activity to take care of managing the given
+ * {@link Cursor}'s lifecycle for you based on the activity's lifecycle.
+ * That is, when the activity is stopped it will automatically call
+ * {@link Cursor#deactivate} on the given Cursor, and when it is later restarted
+ * it will call {@link Cursor#requery} for you. When the activity is
+ * destroyed, all managed Cursors will be closed automatically.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using {@link LoaderManager} instead, available
+ * via {@link #getLoaderManager()}.</em>
+ *
+ * <p><strong>Warning:</strong> Do not call {@link Cursor#close()} on cursor obtained from
+ * {@link #managedQuery}, because the activity will do that for you at the appropriate time.
+ * However, if you call {@link #stopManagingCursor} on a cursor from a managed query, the system
+ * <em>will not</em> automatically close the cursor and, in that case, you must call
+ * {@link Cursor#close()}.</p>
+ *
+ * @param c The Cursor to be managed.
+ *
+ * @see #managedQuery(android.net.Uri , String[], String, String[], String)
+ * @see #stopManagingCursor
+ *
+ * @deprecated Use the new {@link android.content.CursorLoader} class with
+ * {@link LoaderManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public void startManagingCursor(Cursor c) {
+ synchronized (mManagedCursors) {
+ mManagedCursors.add(new ManagedCursor(c));
+ }
+ }
+
+ /**
+ * Given a Cursor that was previously given to
+ * {@link #startManagingCursor}, stop the activity's management of that
+ * cursor.
+ *
+ * <p><strong>Warning:</strong> After calling this method on a cursor from a managed query,
+ * the system <em>will not</em> automatically close the cursor and you must call
+ * {@link Cursor#close()}.</p>
+ *
+ * @param c The Cursor that was being managed.
+ *
+ * @see #startManagingCursor
+ *
+ * @deprecated Use the new {@link android.content.CursorLoader} class with
+ * {@link LoaderManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public void stopManagingCursor(Cursor c) {
+ synchronized (mManagedCursors) {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (mc.mCursor == c) {
+ mManagedCursors.remove(i);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#GINGERBREAD}
+ * this is a no-op.
+ * @hide
+ */
+ @Deprecated
+ public void setPersistent(boolean isPersistent) {
+ }
+
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link #onCreate}.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID if found, or {@code null} otherwise
+ * @see View#findViewById(int)
+ */
+ @Nullable
+ public <T extends View> T findViewById(@IdRes int id) {
+ return getWindow().findViewById(id);
+ }
+
+ /**
+ * Retrieve a reference to this activity's ActionBar.
+ *
+ * @return The Activity's ActionBar, or null if it does not have one.
+ */
+ @Nullable
+ public ActionBar getActionBar() {
+ initWindowDecorActionBar();
+ return mActionBar;
+ }
+
+ /**
+ * Set a {@link android.widget.Toolbar Toolbar} to act as the {@link ActionBar} for this
+ * Activity window.
+ *
+ * <p>When set to a non-null value the {@link #getActionBar()} method will return
+ * an {@link ActionBar} object that can be used to control the given toolbar as if it were
+ * a traditional window decor action bar. The toolbar's menu will be populated with the
+ * Activity's options menu and the navigation button will be wired through the standard
+ * {@link android.R.id#home home} menu select action.</p>
+ *
+ * <p>In order to use a Toolbar within the Activity's window content the application
+ * must not request the window feature {@link Window#FEATURE_ACTION_BAR FEATURE_ACTION_BAR}.</p>
+ *
+ * @param toolbar Toolbar to set as the Activity's action bar, or {@code null} to clear it
+ */
+ public void setActionBar(@Nullable Toolbar toolbar) {
+ final ActionBar ab = getActionBar();
+ if (ab instanceof WindowDecorActionBar) {
+ throw new IllegalStateException("This Activity already has an action bar supplied " +
+ "by the window decor. Do not request Window.FEATURE_ACTION_BAR and set " +
+ "android:windowActionBar to false in your theme to use a Toolbar instead.");
+ }
+
+ // If we reach here then we're setting a new action bar
+ // First clear out the MenuInflater to make sure that it is valid for the new Action Bar
+ mMenuInflater = null;
+
+ // If we have an action bar currently, destroy it
+ if (ab != null) {
+ ab.onDestroy();
+ }
+
+ if (toolbar != null) {
+ final ToolbarActionBar tbab = new ToolbarActionBar(toolbar, getTitle(), this);
+ mActionBar = tbab;
+ mWindow.setCallback(tbab.getWrappedWindowCallback());
+ } else {
+ mActionBar = null;
+ // Re-set the original window callback since we may have already set a Toolbar wrapper
+ mWindow.setCallback(this);
+ }
+
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * Creates a new ActionBar, locates the inflated ActionBarView,
+ * initializes the ActionBar with the view, and sets mActionBar.
+ */
+ private void initWindowDecorActionBar() {
+ Window window = getWindow();
+
+ // Initializing the window decor can change window feature flags.
+ // Make sure that we have the correct set before performing the test below.
+ window.getDecorView();
+
+ if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
+ return;
+ }
+
+ mActionBar = new WindowDecorActionBar(this);
+ mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
+
+ mWindow.setDefaultIcon(mActivityInfo.getIconResource());
+ mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
+ }
+
+ /**
+ * Set the activity content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the activity.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ *
+ * @see #setContentView(android.view.View)
+ * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
+ */
+ public void setContentView(@LayoutRes int layoutResID) {
+ getWindow().setContentView(layoutResID);
+ initWindowDecorActionBar();
+ }
+
+ /**
+ * Set the activity content to an explicit view. This view is placed
+ * directly into the activity's view hierarchy. It can itself be a complex
+ * view hierarchy. When calling this method, the layout parameters of the
+ * specified view are ignored. Both the width and the height of the view are
+ * set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use
+ * your own layout parameters, invoke
+ * {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}
+ * instead.
+ *
+ * @param view The desired content to display.
+ *
+ * @see #setContentView(int)
+ * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
+ */
+ public void setContentView(View view) {
+ getWindow().setContentView(view);
+ initWindowDecorActionBar();
+ }
+
+ /**
+ * Set the activity content to an explicit view. This view is placed
+ * directly into the activity's view hierarchy. It can itself be a complex
+ * view hierarchy.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ *
+ * @see #setContentView(android.view.View)
+ * @see #setContentView(int)
+ */
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().setContentView(view, params);
+ initWindowDecorActionBar();
+ }
+
+ /**
+ * Add an additional content view to the activity. Added after any existing
+ * ones in the activity -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().addContentView(view, params);
+ initWindowDecorActionBar();
+ }
+
+ /**
+ * Retrieve the {@link TransitionManager} responsible for default transitions in this window.
+ * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * <p>This method will return non-null after content has been initialized (e.g. by using
+ * {@link #setContentView}) if {@link Window#FEATURE_CONTENT_TRANSITIONS} has been granted.</p>
+ *
+ * @return This window's content TransitionManager or null if none is set.
+ */
+ public TransitionManager getContentTransitionManager() {
+ return getWindow().getTransitionManager();
+ }
+
+ /**
+ * Set the {@link TransitionManager} to use for default transitions in this window.
+ * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param tm The TransitionManager to use for scene changes.
+ */
+ public void setContentTransitionManager(TransitionManager tm) {
+ getWindow().setTransitionManager(tm);
+ }
+
+ /**
+ * Retrieve the {@link Scene} representing this window's current content.
+ * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * <p>This method will return null if the current content is not represented by a Scene.</p>
+ *
+ * @return Current Scene being shown or null
+ */
+ public Scene getContentScene() {
+ return getWindow().getContentScene();
+ }
+
+ /**
+ * Sets whether this activity is finished when touched outside its window's
+ * bounds.
+ */
+ public void setFinishOnTouchOutside(boolean finish) {
+ mWindow.setCloseOnTouchOutside(finish);
+ }
+
+ /** @hide */
+ @IntDef(prefix = { "DEFAULT_KEYS_" }, value = {
+ DEFAULT_KEYS_DISABLE,
+ DEFAULT_KEYS_DIALER,
+ DEFAULT_KEYS_SHORTCUT,
+ DEFAULT_KEYS_SEARCH_LOCAL,
+ DEFAULT_KEYS_SEARCH_GLOBAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface DefaultKeyMode {}
+
+ /**
+ * Use with {@link #setDefaultKeyMode} to turn off default handling of
+ * keys.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_DISABLE = 0;
+ /**
+ * Use with {@link #setDefaultKeyMode} to launch the dialer during default
+ * key handling.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_DIALER = 1;
+ /**
+ * Use with {@link #setDefaultKeyMode} to execute a menu shortcut in
+ * default key handling.
+ *
+ * <p>That is, the user does not need to hold down the menu key to execute menu shortcuts.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SHORTCUT = 2;
+ /**
+ * Use with {@link #setDefaultKeyMode} to specify that unhandled keystrokes
+ * will start an application-defined search. (If the application or activity does not
+ * actually define a search, the the keys will be ignored.)
+ *
+ * <p>See {@link android.app.SearchManager android.app.SearchManager} for more details.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SEARCH_LOCAL = 3;
+
+ /**
+ * Use with {@link #setDefaultKeyMode} to specify that unhandled keystrokes
+ * will start a global search (typically web search, but some platforms may define alternate
+ * methods for global search)
+ *
+ * <p>See {@link android.app.SearchManager android.app.SearchManager} for more details.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SEARCH_GLOBAL = 4;
+
+ /**
+ * Select the default key handling for this activity. This controls what
+ * will happen to key events that are not otherwise handled. The default
+ * mode ({@link #DEFAULT_KEYS_DISABLE}) will simply drop them on the
+ * floor. Other modes allow you to launch the dialer
+ * ({@link #DEFAULT_KEYS_DIALER}), execute a shortcut in your options
+ * menu without requiring the menu key be held down
+ * ({@link #DEFAULT_KEYS_SHORTCUT}), or launch a search ({@link #DEFAULT_KEYS_SEARCH_LOCAL}
+ * and {@link #DEFAULT_KEYS_SEARCH_GLOBAL}).
+ *
+ * <p>Note that the mode selected here does not impact the default
+ * handling of system keys, such as the "back" and "menu" keys, and your
+ * activity and its views always get a first chance to receive and handle
+ * all application keys.
+ *
+ * @param mode The desired default key mode constant.
+ *
+ * @see #onKeyDown
+ */
+ public final void setDefaultKeyMode(@DefaultKeyMode int mode) {
+ mDefaultKeyMode = mode;
+
+ // Some modes use a SpannableStringBuilder to track & dispatch input events
+ // This list must remain in sync with the switch in onKeyDown()
+ switch (mode) {
+ case DEFAULT_KEYS_DISABLE:
+ case DEFAULT_KEYS_SHORTCUT:
+ mDefaultKeySsb = null; // not used in these modes
+ break;
+ case DEFAULT_KEYS_DIALER:
+ case DEFAULT_KEYS_SEARCH_LOCAL:
+ case DEFAULT_KEYS_SEARCH_GLOBAL:
+ mDefaultKeySsb = new SpannableStringBuilder();
+ Selection.setSelection(mDefaultKeySsb,0);
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Called when a key was pressed down and not handled by any of the views
+ * inside of the activity. So, for example, key presses while the cursor
+ * is inside a TextView will not trigger the event (unless it is a navigation
+ * to another object) because TextView handles its own key presses.
+ *
+ * <p>If the focused view didn't want this event, this method is called.
+ *
+ * <p>The default implementation takes care of {@link KeyEvent#KEYCODE_BACK}
+ * by calling {@link #onBackPressed()}, though the behavior varies based
+ * on the application compatibility mode: for
+ * {@link android.os.Build.VERSION_CODES#ECLAIR} or later applications,
+ * it will set up the dispatch to call {@link #onKeyUp} where the action
+ * will be performed; for earlier applications, it will perform the
+ * action immediately in on-down, as those versions of the platform
+ * behaved.
+ *
+ * <p>Other additional default key handling may be performed
+ * if configured with {@link #setDefaultKeyMode}.
+ *
+ * @return Return <code>true</code> to prevent this event from being propagated
+ * further, or <code>false</code> to indicate that you have not handled
+ * this event and it should continue to be propagated.
+ * @see #onKeyUp
+ * @see android.view.KeyEvent
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (getApplicationInfo().targetSdkVersion
+ >= Build.VERSION_CODES.ECLAIR) {
+ event.startTracking();
+ } else {
+ onBackPressed();
+ }
+ return true;
+ }
+
+ if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
+ return false;
+ } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
+ Window w = getWindow();
+ if (w.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
+ w.performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, keyCode, event,
+ Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {
+ return true;
+ }
+ return false;
+ } else if (keyCode == KeyEvent.KEYCODE_TAB) {
+ // Don't consume TAB here since it's used for navigation. Arrow keys
+ // aren't considered "typing keys" so they already won't get consumed.
+ return false;
+ } else {
+ // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
+ boolean clearSpannable = false;
+ boolean handled;
+ if ((event.getRepeatCount() != 0) || event.isSystem()) {
+ clearSpannable = true;
+ handled = false;
+ } else {
+ handled = TextKeyListener.getInstance().onKeyDown(
+ null, mDefaultKeySsb, keyCode, event);
+ if (handled && mDefaultKeySsb.length() > 0) {
+ // something useable has been typed - dispatch it now.
+
+ final String str = mDefaultKeySsb.toString();
+ clearSpannable = true;
+
+ switch (mDefaultKeyMode) {
+ case DEFAULT_KEYS_DIALER:
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + str));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ break;
+ case DEFAULT_KEYS_SEARCH_LOCAL:
+ startSearch(str, false, null, false);
+ break;
+ case DEFAULT_KEYS_SEARCH_GLOBAL:
+ startSearch(str, false, null, true);
+ break;
+ }
+ }
+ }
+ if (clearSpannable) {
+ mDefaultKeySsb.clear();
+ mDefaultKeySsb.clearSpans();
+ Selection.setSelection(mDefaultKeySsb,0);
+ }
+ return handled;
+ }
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
+ * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
+ * the event).
+ */
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a key was released and not handled by any of the views
+ * inside of the activity. So, for example, key presses while the cursor
+ * is inside a TextView will not trigger the event (unless it is a navigation
+ * to another object) because TextView handles its own key presses.
+ *
+ * <p>The default implementation handles KEYCODE_BACK to stop the activity
+ * and go back.
+ *
+ * @return Return <code>true</code> to prevent this event from being propagated
+ * further, or <code>false</code> to indicate that you have not handled
+ * this event and it should continue to be propagated.
+ * @see #onKeyDown
+ * @see KeyEvent
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (getApplicationInfo().targetSdkVersion
+ >= Build.VERSION_CODES.ECLAIR) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
+ && !event.isCanceled()) {
+ onBackPressed();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ */
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when the activity has detected the user's press of the back
+ * key. The default implementation simply finishes the current activity,
+ * but you can override this to do whatever you want.
+ */
+ public void onBackPressed() {
+ if (mActionBar != null && mActionBar.collapseActionView()) {
+ return;
+ }
+
+ FragmentManager fragmentManager = mFragments.getFragmentManager();
+
+ if (fragmentManager.isStateSaved() || !fragmentManager.popBackStackImmediate()) {
+ finishAfterTransition();
+ }
+ }
+
+ /**
+ * Called when a key shortcut event is not handled by any of the views in the Activity.
+ * Override this method to implement global key shortcuts for the Activity.
+ * Key shortcuts can also be implemented by setting the
+ * {@link MenuItem#setShortcut(char, char) shortcut} property of menu items.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ * @return True if the key shortcut was handled.
+ */
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ // Let the Action Bar have a chance at handling the shortcut.
+ ActionBar actionBar = getActionBar();
+ return (actionBar != null && actionBar.onKeyShortcut(keyCode, event));
+ }
+
+ /**
+ * Called when a touch screen event was not handled by any of the views
+ * under it. This is most useful to process touch events that happen
+ * outside of your window bounds, where there is no view to receive it.
+ *
+ * @param event The touch screen event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mWindow.shouldCloseOnTouch(this, event)) {
+ finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when the trackball was moved and not handled by any of the
+ * views inside of the activity. So, for example, if the trackball moves
+ * while focus is on a button, you will receive a call here because
+ * buttons do not normally do anything with trackball events. The call
+ * here happens <em>before</em> trackball movements are converted to
+ * DPAD key events, which then get sent back to the view hierarchy, and
+ * will be processed at the point for things like focus navigation.
+ *
+ * @param event The trackball event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a generic motion event was not handled by any of the
+ * views inside of the activity.
+ * <p>
+ * Generic motion events describe joystick movements, mouse hovers, track pad
+ * touches, scroll wheel movements and other input events. The
+ * {@link MotionEvent#getSource() source} of the motion event specifies
+ * the class of input that was received. Implementations of this method
+ * must examine the bits in the source before processing the event.
+ * The following code example shows how this is done.
+ * </p><p>
+ * Generic motion events with source class
+ * {@link android.view.InputDevice#SOURCE_CLASS_POINTER}
+ * are delivered to the view under the pointer. All other generic motion events are
+ * delivered to the focused view.
+ * </p><p>
+ * See {@link View#onGenericMotionEvent(MotionEvent)} for an example of how to
+ * handle this event.
+ * </p>
+ *
+ * @param event The generic motion event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Called whenever a key, touch, or trackball event is dispatched to the
+ * activity. Implement this method if you wish to know that the user has
+ * interacted with the device in some way while your activity is running.
+ * This callback and {@link #onUserLeaveHint} are intended to help
+ * activities manage status bar notifications intelligently; specifically,
+ * for helping activities determine the proper time to cancel a notfication.
+ *
+ * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
+ * be accompanied by calls to {@link #onUserInteraction}. This
+ * ensures that your activity will be told of relevant user activity such
+ * as pulling down the notification pane and touching an item there.
+ *
+ * <p>Note that this callback will be invoked for the touch down action
+ * that begins a touch gesture, but may not be invoked for the touch-moved
+ * and touch-up actions that follow.
+ *
+ * @see #onUserLeaveHint()
+ */
+ public void onUserInteraction() {
+ }
+
+ public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
+ // Update window manager if: we have a view, that view is
+ // attached to its parent (which will be a RootView), and
+ // this activity is not embedded.
+ if (mParent == null) {
+ View decor = mDecor;
+ if (decor != null && decor.getParent() != null) {
+ getWindowManager().updateViewLayout(decor, params);
+ }
+ }
+ }
+
+ public void onContentChanged() {
+ }
+
+ /**
+ * Called when the current {@link Window} of the activity gains or loses
+ * focus. This is the best indicator of whether this activity is visible
+ * to the user. The default implementation clears the key tracking
+ * state, so should always be called.
+ *
+ * <p>Note that this provides information about global focus state, which
+ * is managed independently of activity lifecycles. As such, while focus
+ * changes will generally have some relation to lifecycle changes (an
+ * activity that is stopped will not generally get window focus), you
+ * should not rely on any particular order between the callbacks here and
+ * those in the other lifecycle methods such as {@link #onResume}.
+ *
+ * <p>As a general rule, however, a resumed activity will have window
+ * focus... unless it has displayed other dialogs or popups that take
+ * input focus, in which case the activity itself will not have focus
+ * when the other windows have it. Likewise, the system may display
+ * system-level windows (such as the status bar notification panel or
+ * a system alert) which will temporarily take window input focus without
+ * pausing the foreground activity.
+ *
+ * @param hasFocus Whether the window of this activity has focus.
+ *
+ * @see #hasWindowFocus()
+ * @see #onResume
+ * @see View#onWindowFocusChanged(boolean)
+ */
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ /**
+ * Called when the main window associated with the activity has been
+ * attached to the window manager.
+ * See {@link View#onAttachedToWindow() View.onAttachedToWindow()}
+ * for more information.
+ * @see View#onAttachedToWindow
+ */
+ public void onAttachedToWindow() {
+ }
+
+ /**
+ * Called when the main window associated with the activity has been
+ * detached from the window manager.
+ * See {@link View#onDetachedFromWindow() View.onDetachedFromWindow()}
+ * for more information.
+ * @see View#onDetachedFromWindow
+ */
+ public void onDetachedFromWindow() {
+ }
+
+ /**
+ * Returns true if this activity's <em>main</em> window currently has window focus.
+ * Note that this is not the same as the view itself having focus.
+ *
+ * @return True if this activity's main window currently has window focus.
+ *
+ * @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams)
+ */
+ public boolean hasWindowFocus() {
+ Window w = getWindow();
+ if (w != null) {
+ View d = w.getDecorView();
+ if (d != null) {
+ return d.hasWindowFocus();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when the main window associated with the activity has been dismissed.
+ * @hide
+ */
+ @Override
+ public void onWindowDismissed(boolean finishTask, boolean suppressWindowTransition) {
+ finish(finishTask ? FINISH_TASK_WITH_ACTIVITY : DONT_FINISH_TASK_WITH_ACTIVITY);
+ if (suppressWindowTransition) {
+ overridePendingTransition(0, 0);
+ }
+ }
+
+
+ /**
+ * Moves the activity from {@link WindowConfiguration#WINDOWING_MODE_FREEFORM} windowing mode to
+ * {@link WindowConfiguration#WINDOWING_MODE_FULLSCREEN}.
+ *
+ * @hide
+ */
+ @Override
+ public void exitFreeformMode() throws RemoteException {
+ ActivityManager.getService().exitFreeformMode(mToken);
+ }
+
+ /**
+ * Puts the activity in picture-in-picture mode if the activity supports.
+ * @see android.R.attr#supportsPictureInPicture
+ * @hide
+ */
+ @Override
+ public void enterPictureInPictureModeIfPossible() {
+ if (mActivityInfo.supportsPictureInPicture()) {
+ enterPictureInPictureMode();
+ }
+ }
+
+ /**
+ * Called to process key events. You can override this to intercept all
+ * key events before they are dispatched to the window. Be sure to call
+ * this implementation for key events that should be handled normally.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ onUserInteraction();
+
+ // Let action bars open menus in response to the menu key prioritized over
+ // the window handling it
+ final int keyCode = event.getKeyCode();
+ if (keyCode == KeyEvent.KEYCODE_MENU &&
+ mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
+ return true;
+ }
+
+ Window win = getWindow();
+ if (win.superDispatchKeyEvent(event)) {
+ return true;
+ }
+ View decor = mDecor;
+ if (decor == null) decor = win.getDecorView();
+ return event.dispatch(this, decor != null
+ ? decor.getKeyDispatcherState() : null, this);
+ }
+
+ /**
+ * Called to process a key shortcut event.
+ * You can override this to intercept all key shortcut events before they are
+ * dispatched to the window. Be sure to call this implementation for key shortcut
+ * events that should be handled normally.
+ *
+ * @param event The key shortcut event.
+ * @return True if this event was consumed.
+ */
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ onUserInteraction();
+ if (getWindow().superDispatchKeyShortcutEvent(event)) {
+ return true;
+ }
+ return onKeyShortcut(event.getKeyCode(), event);
+ }
+
+ /**
+ * Called to process touch screen events. You can override this to
+ * intercept all touch screen events before they are dispatched to the
+ * window. Be sure to call this implementation for touch screen events
+ * that should be handled normally.
+ *
+ * @param ev The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ onUserInteraction();
+ }
+ if (getWindow().superDispatchTouchEvent(ev)) {
+ return true;
+ }
+ return onTouchEvent(ev);
+ }
+
+ /**
+ * Called to process trackball events. You can override this to
+ * intercept all trackball events before they are dispatched to the
+ * window. Be sure to call this implementation for trackball events
+ * that should be handled normally.
+ *
+ * @param ev The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ onUserInteraction();
+ if (getWindow().superDispatchTrackballEvent(ev)) {
+ return true;
+ }
+ return onTrackballEvent(ev);
+ }
+
+ /**
+ * Called to process generic motion events. You can override this to
+ * intercept all generic motion events before they are dispatched to the
+ * window. Be sure to call this implementation for generic motion events
+ * that should be handled normally.
+ *
+ * @param ev The generic motion event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ onUserInteraction();
+ if (getWindow().superDispatchGenericMotionEvent(ev)) {
+ return true;
+ }
+ return onGenericMotionEvent(ev);
+ }
+
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ event.setClassName(getClass().getName());
+ event.setPackageName(getPackageName());
+
+ LayoutParams params = getWindow().getAttributes();
+ boolean isFullScreen = (params.width == LayoutParams.MATCH_PARENT) &&
+ (params.height == LayoutParams.MATCH_PARENT);
+ event.setFullScreen(isFullScreen);
+
+ CharSequence title = getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ event.getText().add(title);
+ }
+
+ return true;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onCreatePanelView}
+ * for activities. This
+ * simply returns null so that all panel sub-windows will have the default
+ * menu behavior.
+ */
+ @Nullable
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onCreatePanelMenu}
+ * for activities. This calls through to the new
+ * {@link #onCreateOptionsMenu} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL} panel,
+ * so that subclasses of Activity don't need to deal with feature codes.
+ */
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ boolean show = onCreateOptionsMenu(menu);
+ show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater());
+ return show;
+ }
+ return false;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onPreparePanel}
+ * for activities. This
+ * calls through to the new {@link #onPrepareOptionsMenu} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL}
+ * panel, so that subclasses of
+ * Activity don't need to deal with feature codes.
+ */
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
+ boolean goforit = onPrepareOptionsMenu(menu);
+ goforit |= mFragments.dispatchPrepareOptionsMenu(menu);
+ return goforit;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return The default implementation returns true.
+ */
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_ACTION_BAR) {
+ initWindowDecorActionBar();
+ if (mActionBar != null) {
+ mActionBar.dispatchMenuVisibilityChanged(true);
+ } else {
+ Log.e(TAG, "Tried to open action bar menu with no action bar");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onMenuItemSelected}
+ * for activities. This calls through to the new
+ * {@link #onOptionsItemSelected} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL}
+ * panel, so that subclasses of
+ * Activity don't need to deal with feature codes.
+ */
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ CharSequence titleCondensed = item.getTitleCondensed();
+
+ switch (featureId) {
+ case Window.FEATURE_OPTIONS_PANEL:
+ // Put event logging here so it gets called even if subclass
+ // doesn't call through to superclass's implmeentation of each
+ // of these methods below
+ if(titleCondensed != null) {
+ EventLog.writeEvent(50000, 0, titleCondensed.toString());
+ }
+ if (onOptionsItemSelected(item)) {
+ return true;
+ }
+ if (mFragments.dispatchOptionsItemSelected(item)) {
+ return true;
+ }
+ if (item.getItemId() == android.R.id.home && mActionBar != null &&
+ (mActionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
+ if (mParent == null) {
+ return onNavigateUp();
+ } else {
+ return mParent.onNavigateUpFromChild(this);
+ }
+ }
+ return false;
+
+ case Window.FEATURE_CONTEXT_MENU:
+ if(titleCondensed != null) {
+ EventLog.writeEvent(50000, 1, titleCondensed.toString());
+ }
+ if (onContextItemSelected(item)) {
+ return true;
+ }
+ return mFragments.dispatchContextItemSelected(item);
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onPanelClosed(int, Menu)} for
+ * activities. This calls through to {@link #onOptionsMenuClosed(Menu)}
+ * method for the {@link android.view.Window#FEATURE_OPTIONS_PANEL} panel,
+ * so that subclasses of Activity don't need to deal with feature codes.
+ * For context menus ({@link Window#FEATURE_CONTEXT_MENU}), the
+ * {@link #onContextMenuClosed(Menu)} will be called.
+ */
+ public void onPanelClosed(int featureId, Menu menu) {
+ switch (featureId) {
+ case Window.FEATURE_OPTIONS_PANEL:
+ mFragments.dispatchOptionsMenuClosed(menu);
+ onOptionsMenuClosed(menu);
+ break;
+
+ case Window.FEATURE_CONTEXT_MENU:
+ onContextMenuClosed(menu);
+ break;
+
+ case Window.FEATURE_ACTION_BAR:
+ initWindowDecorActionBar();
+ mActionBar.dispatchMenuVisibilityChanged(false);
+ break;
+ }
+ }
+
+ /**
+ * Declare that the options menu has changed, so should be recreated.
+ * The {@link #onCreateOptionsMenu(Menu)} method will be called the next
+ * time it needs to be displayed.
+ */
+ public void invalidateOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
+ (mActionBar == null || !mActionBar.invalidateOptionsMenu())) {
+ mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL);
+ }
+ }
+
+ /**
+ * Initialize the contents of the Activity's standard options menu. You
+ * should place your menu items in to <var>menu</var>.
+ *
+ * <p>This is only called once, the first time the options menu is
+ * displayed. To update the menu every time it is displayed, see
+ * {@link #onPrepareOptionsMenu}.
+ *
+ * <p>The default implementation populates the menu with standard system
+ * menu items. These are placed in the {@link Menu#CATEGORY_SYSTEM} group so that
+ * they will be correctly ordered with application-defined menu items.
+ * Deriving classes should always call through to the base implementation.
+ *
+ * <p>You can safely hold on to <var>menu</var> (and any items created
+ * from it), making modifications to it as desired, until the next
+ * time onCreateOptionsMenu() is called.
+ *
+ * <p>When you add items to the menu, you can implement the Activity's
+ * {@link #onOptionsItemSelected} method to handle them there.
+ *
+ * @param menu The options menu in which you place your items.
+ *
+ * @return You must return true for the menu to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onPrepareOptionsMenu
+ * @see #onOptionsItemSelected
+ */
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mParent != null) {
+ return mParent.onCreateOptionsMenu(menu);
+ }
+ return true;
+ }
+
+ /**
+ * Prepare the Screen's standard options menu to be displayed. This is
+ * called right before the menu is shown, every time it is shown. You can
+ * use this method to efficiently enable/disable items or otherwise
+ * dynamically modify the contents.
+ *
+ * <p>The default implementation updates the system menu items based on the
+ * activity's state. Deriving classes should always call through to the
+ * base class implementation.
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ *
+ * @return You must return true for the menu to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ if (mParent != null) {
+ return mParent.onPrepareOptionsMenu(menu);
+ }
+ return true;
+ }
+
+ /**
+ * This hook is called whenever an item in your options menu is selected.
+ * The default implementation simply returns false to have the normal
+ * processing happen (calling the item's Runnable or sending a message to
+ * its Handler as appropriate). You can use this method for any items
+ * for which you would like to do processing without those other
+ * facilities.
+ *
+ * <p>Derived classes should call through to the base class for it to
+ * perform the default menu handling.</p>
+ *
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return false to allow normal menu processing to
+ * proceed, true to consume it here.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mParent != null) {
+ return mParent.onOptionsItemSelected(item);
+ }
+ return false;
+ }
+
+ /**
+ * This method is called whenever the user chooses to navigate Up within your application's
+ * activity hierarchy from the action bar.
+ *
+ * <p>If the attribute {@link android.R.attr#parentActivityName parentActivityName}
+ * was specified in the manifest for this activity or an activity-alias to it,
+ * default Up navigation will be handled automatically. If any activity
+ * along the parent chain requires extra Intent arguments, the Activity subclass
+ * should override the method {@link #onPrepareNavigateUpTaskStack(TaskStackBuilder)}
+ * to supply those arguments.</p>
+ *
+ * <p>See <a href="{@docRoot}guide/components/tasks-and-back-stack.html">Tasks and Back Stack</a>
+ * from the developer guide and <a href="{@docRoot}design/patterns/navigation.html">Navigation</a>
+ * from the design guide for more information about navigating within your app.</p>
+ *
+ * <p>See the {@link TaskStackBuilder} class and the Activity methods
+ * {@link #getParentActivityIntent()}, {@link #shouldUpRecreateTask(Intent)}, and
+ * {@link #navigateUpTo(Intent)} for help implementing custom Up navigation.
+ * The AppNavigation sample application in the Android SDK is also available for reference.</p>
+ *
+ * @return true if Up navigation completed successfully and this Activity was finished,
+ * false otherwise.
+ */
+ public boolean onNavigateUp() {
+ // Automatically handle hierarchical Up navigation if the proper
+ // metadata is available.
+ Intent upIntent = getParentActivityIntent();
+ if (upIntent != null) {
+ if (mActivityInfo.taskAffinity == null) {
+ // Activities with a null affinity are special; they really shouldn't
+ // specify a parent activity intent in the first place. Just finish
+ // the current activity and call it a day.
+ finish();
+ } else if (shouldUpRecreateTask(upIntent)) {
+ TaskStackBuilder b = TaskStackBuilder.create(this);
+ onCreateNavigateUpTaskStack(b);
+ onPrepareNavigateUpTaskStack(b);
+ b.startActivities();
+
+ // We can't finishAffinity if we have a result.
+ // Fall back and simply finish the current activity instead.
+ if (mResultCode != RESULT_CANCELED || mResultData != null) {
+ // Tell the developer what's going on to avoid hair-pulling.
+ Log.i(TAG, "onNavigateUp only finishing topmost activity to return a result");
+ finish();
+ } else {
+ finishAffinity();
+ }
+ } else {
+ navigateUpTo(upIntent);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This is called when a child activity of this one attempts to navigate up.
+ * The default implementation simply calls onNavigateUp() on this activity (the parent).
+ *
+ * @param child The activity making the call.
+ */
+ public boolean onNavigateUpFromChild(Activity child) {
+ return onNavigateUp();
+ }
+
+ /**
+ * Define the synthetic task stack that will be generated during Up navigation from
+ * a different task.
+ *
+ * <p>The default implementation of this method adds the parent chain of this activity
+ * as specified in the manifest to the supplied {@link TaskStackBuilder}. Applications
+ * may choose to override this method to construct the desired task stack in a different
+ * way.</p>
+ *
+ * <p>This method will be invoked by the default implementation of {@link #onNavigateUp()}
+ * if {@link #shouldUpRecreateTask(Intent)} returns true when supplied with the intent
+ * returned by {@link #getParentActivityIntent()}.</p>
+ *
+ * <p>Applications that wish to supply extra Intent parameters to the parent stack defined
+ * by the manifest should override {@link #onPrepareNavigateUpTaskStack(TaskStackBuilder)}.</p>
+ *
+ * @param builder An empty TaskStackBuilder - the application should add intents representing
+ * the desired task stack
+ */
+ public void onCreateNavigateUpTaskStack(TaskStackBuilder builder) {
+ builder.addParentStack(this);
+ }
+
+ /**
+ * Prepare the synthetic task stack that will be generated during Up navigation
+ * from a different task.
+ *
+ * <p>This method receives the {@link TaskStackBuilder} with the constructed series of
+ * Intents as generated by {@link #onCreateNavigateUpTaskStack(TaskStackBuilder)}.
+ * If any extra data should be added to these intents before launching the new task,
+ * the application should override this method and add that data here.</p>
+ *
+ * @param builder A TaskStackBuilder that has been populated with Intents by
+ * onCreateNavigateUpTaskStack.
+ */
+ public void onPrepareNavigateUpTaskStack(TaskStackBuilder builder) {
+ }
+
+ /**
+ * This hook is called whenever the options menu is being closed (either by the user canceling
+ * the menu with the back/menu button, or when an item is selected).
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ */
+ public void onOptionsMenuClosed(Menu menu) {
+ if (mParent != null) {
+ mParent.onOptionsMenuClosed(menu);
+ }
+ }
+
+ /**
+ * Programmatically opens the options menu. If the options menu is already
+ * open, this method does nothing.
+ */
+ public void openOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
+ (mActionBar == null || !mActionBar.openOptionsMenu())) {
+ mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
+ }
+ }
+
+ /**
+ * Progammatically closes the options menu. If the options menu is already
+ * closed, this method does nothing.
+ */
+ public void closeOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
+ (mActionBar == null || !mActionBar.closeOptionsMenu())) {
+ mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL);
+ }
+ }
+
+ /**
+ * Called when a context menu for the {@code view} is about to be shown.
+ * Unlike {@link #onCreateOptionsMenu(Menu)}, this will be called every
+ * time the context menu is about to be shown and should be populated for
+ * the view (or item inside the view for {@link AdapterView} subclasses,
+ * this can be found in the {@code menuInfo})).
+ * <p>
+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an
+ * item has been selected.
+ * <p>
+ * It is not safe to hold onto the context menu after this method returns.
+ *
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Registers a context menu to be shown for the given view (multiple views
+ * can show the context menu). This method will set the
+ * {@link OnCreateContextMenuListener} on the view to this activity, so
+ * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be
+ * called when it is time to show the context menu.
+ *
+ * @see #unregisterForContextMenu(View)
+ * @param view The view that should show a context menu.
+ */
+ public void registerForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * Prevents a context menu to be shown for the given view. This method will remove the
+ * {@link OnCreateContextMenuListener} on the view.
+ *
+ * @see #registerForContextMenu(View)
+ * @param view The view that should stop showing a context menu.
+ */
+ public void unregisterForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * Programmatically opens the context menu for a particular {@code view}.
+ * The {@code view} should have been added via
+ * {@link #registerForContextMenu(View)}.
+ *
+ * @param view The view to show the context menu for.
+ */
+ public void openContextMenu(View view) {
+ view.showContextMenu();
+ }
+
+ /**
+ * Programmatically closes the most recently opened context menu, if showing.
+ */
+ public void closeContextMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_CONTEXT_MENU)) {
+ mWindow.closePanel(Window.FEATURE_CONTEXT_MENU);
+ }
+ }
+
+ /**
+ * This hook is called whenever an item in a context menu is selected. The
+ * default implementation simply returns false to have the normal processing
+ * happen (calling the item's Runnable or sending a message to its Handler
+ * as appropriate). You can use this method for any items for which you
+ * would like to do processing without those other facilities.
+ * <p>
+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the
+ * View that added this menu item.
+ * <p>
+ * Derived classes should call through to the base class for it to perform
+ * the default menu handling.
+ *
+ * @param item The context menu item that was selected.
+ * @return boolean Return false to allow normal context menu processing to
+ * proceed, true to consume it here.
+ */
+ public boolean onContextItemSelected(MenuItem item) {
+ if (mParent != null) {
+ return mParent.onContextItemSelected(item);
+ }
+ return false;
+ }
+
+ /**
+ * This hook is called whenever the context menu is being closed (either by
+ * the user canceling the menu with the back/menu button, or when an item is
+ * selected).
+ *
+ * @param menu The context menu that is being closed.
+ */
+ public void onContextMenuClosed(Menu menu) {
+ if (mParent != null) {
+ mParent.onContextMenuClosed(menu);
+ }
+ }
+
+ /**
+ * @deprecated Old no-arguments version of {@link #onCreateDialog(int, Bundle)}.
+ */
+ @Deprecated
+ protected Dialog onCreateDialog(int id) {
+ return null;
+ }
+
+ /**
+ * Callback for creating dialogs that are managed (saved and restored) for you
+ * by the activity. The default implementation calls through to
+ * {@link #onCreateDialog(int)} for compatibility.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using a {@link DialogFragment} instead.</em>
+ *
+ * <p>If you use {@link #showDialog(int)}, the activity will call through to
+ * this method the first time, and hang onto it thereafter. Any dialog
+ * that is created by this method will automatically be saved and restored
+ * for you, including whether it is showing.
+ *
+ * <p>If you would like the activity to manage saving and restoring dialogs
+ * for you, you should override this method and handle any ids that are
+ * passed to {@link #showDialog}.
+ *
+ * <p>If you would like an opportunity to prepare your dialog before it is shown,
+ * override {@link #onPrepareDialog(int, Dialog, Bundle)}.
+ *
+ * @param id The id of the dialog.
+ * @param args The dialog arguments provided to {@link #showDialog(int, Bundle)}.
+ * @return The dialog. If you return null, the dialog will not be created.
+ *
+ * @see #onPrepareDialog(int, Dialog, Bundle)
+ * @see #showDialog(int, Bundle)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Nullable
+ @Deprecated
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ return onCreateDialog(id);
+ }
+
+ /**
+ * @deprecated Old no-arguments version of
+ * {@link #onPrepareDialog(int, Dialog, Bundle)}.
+ */
+ @Deprecated
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ dialog.setOwnerActivity(this);
+ }
+
+ /**
+ * Provides an opportunity to prepare a managed dialog before it is being
+ * shown. The default implementation calls through to
+ * {@link #onPrepareDialog(int, Dialog)} for compatibility.
+ *
+ * <p>
+ * Override this if you need to update a managed dialog based on the state
+ * of the application each time it is shown. For example, a time picker
+ * dialog might want to be updated with the current time. You should call
+ * through to the superclass's implementation. The default implementation
+ * will set this Activity as the owner activity on the Dialog.
+ *
+ * @param id The id of the managed dialog.
+ * @param dialog The dialog.
+ * @param args The dialog arguments provided to {@link #showDialog(int, Bundle)}.
+ * @see #onCreateDialog(int, Bundle)
+ * @see #showDialog(int)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+ onPrepareDialog(id, dialog);
+ }
+
+ /**
+ * Simple version of {@link #showDialog(int, Bundle)} that does not
+ * take any arguments. Simply calls {@link #showDialog(int, Bundle)}
+ * with null arguments.
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public final void showDialog(int id) {
+ showDialog(id, null);
+ }
+
+ /**
+ * Show a dialog managed by this activity. A call to {@link #onCreateDialog(int, Bundle)}
+ * will be made with the same id the first time this is called for a given
+ * id. From thereafter, the dialog will be automatically saved and restored.
+ *
+ * <em>If you are targeting {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, consider instead using a {@link DialogFragment} instead.</em>
+ *
+ * <p>Each time a dialog is shown, {@link #onPrepareDialog(int, Dialog, Bundle)} will
+ * be made to provide an opportunity to do any timely preparation.
+ *
+ * @param id The id of the managed dialog.
+ * @param args Arguments to pass through to the dialog. These will be saved
+ * and restored for you. Note that if the dialog is already created,
+ * {@link #onCreateDialog(int, Bundle)} will not be called with the new
+ * arguments but {@link #onPrepareDialog(int, Dialog, Bundle)} will be.
+ * If you need to rebuild the dialog, call {@link #removeDialog(int)} first.
+ * @return Returns true if the Dialog was created; false is returned if
+ * it is not created because {@link #onCreateDialog(int, Bundle)} returns false.
+ *
+ * @see Dialog
+ * @see #onCreateDialog(int, Bundle)
+ * @see #onPrepareDialog(int, Dialog, Bundle)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public final boolean showDialog(int id, Bundle args) {
+ if (mManagedDialogs == null) {
+ mManagedDialogs = new SparseArray<ManagedDialog>();
+ }
+ ManagedDialog md = mManagedDialogs.get(id);
+ if (md == null) {
+ md = new ManagedDialog();
+ md.mDialog = createDialog(id, null, args);
+ if (md.mDialog == null) {
+ return false;
+ }
+ mManagedDialogs.put(id, md);
+ }
+
+ md.mArgs = args;
+ onPrepareDialog(id, md.mDialog, args);
+ md.mDialog.show();
+ return true;
+ }
+
+ /**
+ * Dismiss a dialog that was previously shown via {@link #showDialog(int)}.
+ *
+ * @param id The id of the managed dialog.
+ *
+ * @throws IllegalArgumentException if the id was not previously shown via
+ * {@link #showDialog(int)}.
+ *
+ * @see #onCreateDialog(int, Bundle)
+ * @see #onPrepareDialog(int, Dialog, Bundle)
+ * @see #showDialog(int)
+ * @see #removeDialog(int)
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public final void dismissDialog(int id) {
+ if (mManagedDialogs == null) {
+ throw missingDialog(id);
+ }
+
+ final ManagedDialog md = mManagedDialogs.get(id);
+ if (md == null) {
+ throw missingDialog(id);
+ }
+ md.mDialog.dismiss();
+ }
+
+ /**
+ * Creates an exception to throw if a user passed in a dialog id that is
+ * unexpected.
+ */
+ private IllegalArgumentException missingDialog(int id) {
+ return new IllegalArgumentException("no dialog with id " + id + " was ever "
+ + "shown via Activity#showDialog");
+ }
+
+ /**
+ * Removes any internal references to a dialog managed by this Activity.
+ * If the dialog is showing, it will dismiss it as part of the clean up.
+ *
+ * <p>This can be useful if you know that you will never show a dialog again and
+ * want to avoid the overhead of saving and restoring it in the future.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#GINGERBREAD}, this function
+ * will not throw an exception if you try to remove an ID that does not
+ * currently have an associated dialog.</p>
+ *
+ * @param id The id of the managed dialog.
+ *
+ * @see #onCreateDialog(int, Bundle)
+ * @see #onPrepareDialog(int, Dialog, Bundle)
+ * @see #showDialog(int)
+ * @see #dismissDialog(int)
+ *
+ * @deprecated Use the new {@link DialogFragment} class with
+ * {@link FragmentManager} instead; this is also
+ * available on older platforms through the Android compatibility package.
+ */
+ @Deprecated
+ public final void removeDialog(int id) {
+ if (mManagedDialogs != null) {
+ final ManagedDialog md = mManagedDialogs.get(id);
+ if (md != null) {
+ md.mDialog.dismiss();
+ mManagedDialogs.remove(id);
+ }
+ }
+ }
+
+ /**
+ * This hook is called when the user signals the desire to start a search.
+ *
+ * <p>You can use this function as a simple way to launch the search UI, in response to a
+ * menu item, search button, or other widgets within your activity. Unless overidden,
+ * calling this function is the same as calling
+ * {@link #startSearch startSearch(null, false, null, false)}, which launches
+ * search for the current activity as specified in its manifest, see {@link SearchManager}.
+ *
+ * <p>You can override this function to force global search, e.g. in response to a dedicated
+ * search key, or to block search entirely (by simply returning false).
+ *
+ * <p>Note: when running in a {@link Configuration#UI_MODE_TYPE_TELEVISION}, the default
+ * implementation changes to simply return false and you must supply your own custom
+ * implementation if you want to support search.</p>
+ *
+ * @param searchEvent The {@link SearchEvent} that signaled this search.
+ * @return Returns {@code true} if search launched, and {@code false} if the activity does
+ * not respond to search. The default implementation always returns {@code true}, except
+ * when in {@link Configuration#UI_MODE_TYPE_TELEVISION} mode where it returns false.
+ *
+ * @see android.app.SearchManager
+ */
+ public boolean onSearchRequested(@Nullable SearchEvent searchEvent) {
+ mSearchEvent = searchEvent;
+ boolean result = onSearchRequested();
+ mSearchEvent = null;
+ return result;
+ }
+
+ /**
+ * @see #onSearchRequested(SearchEvent)
+ */
+ public boolean onSearchRequested() {
+ if ((getResources().getConfiguration().uiMode&Configuration.UI_MODE_TYPE_MASK)
+ != Configuration.UI_MODE_TYPE_TELEVISION) {
+ startSearch(null, false, null, false);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * During the onSearchRequested() callbacks, this function will return the
+ * {@link SearchEvent} that triggered the callback, if it exists.
+ *
+ * @return SearchEvent The SearchEvent that triggered the {@link
+ * #onSearchRequested} callback.
+ */
+ public final SearchEvent getSearchEvent() {
+ return mSearchEvent;
+ }
+
+ /**
+ * This hook is called to launch the search UI.
+ *
+ * <p>It is typically called from onSearchRequested(), either directly from
+ * Activity.onSearchRequested() or from an overridden version in any given
+ * Activity. If your goal is simply to activate search, it is preferred to call
+ * onSearchRequested(), which may have been overridden elsewhere in your Activity. If your goal
+ * is to inject specific data such as context data, it is preferred to <i>override</i>
+ * onSearchRequested(), so that any callers to it will benefit from the override.
+ *
+ * @param initialQuery Any non-null non-empty string will be inserted as
+ * pre-entered text in the search query box.
+ * @param selectInitialQuery If true, the initial query will be preselected, which means that
+ * any further typing will replace it. This is useful for cases where an entire pre-formed
+ * query is being inserted. If false, the selection point will be placed at the end of the
+ * inserted query. This is useful when the inserted query is text that the user entered,
+ * and the user would expect to be able to keep typing. <i>This parameter is only meaningful
+ * if initialQuery is a non-empty string.</i>
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ * @param globalSearch If false, this will only launch the search that has been specifically
+ * defined by the application (which is usually defined as a local search). If no default
+ * search is defined in the current application or activity, global search will be launched.
+ * If true, this will always launch a platform-global (e.g. web-based) search instead.
+ *
+ * @see android.app.SearchManager
+ * @see #onSearchRequested
+ */
+ public void startSearch(@Nullable String initialQuery, boolean selectInitialQuery,
+ @Nullable Bundle appSearchData, boolean globalSearch) {
+ ensureSearchManager();
+ mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(),
+ appSearchData, globalSearch);
+ }
+
+ /**
+ * Similar to {@link #startSearch}, but actually fires off the search query after invoking
+ * the search dialog. Made available for testing purposes.
+ *
+ * @param query The query to trigger. If empty, the request will be ignored.
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ */
+ public void triggerSearch(String query, @Nullable Bundle appSearchData) {
+ ensureSearchManager();
+ mSearchManager.triggerSearch(query, getComponentName(), appSearchData);
+ }
+
+ /**
+ * Request that key events come to this activity. Use this if your
+ * activity has no views with focus, but the activity still wants
+ * a chance to process key events.
+ *
+ * @see android.view.Window#takeKeyEvents
+ */
+ public void takeKeyEvents(boolean get) {
+ getWindow().takeKeyEvents(get);
+ }
+
+ /**
+ * Enable extended window features. This is a convenience for calling
+ * {@link android.view.Window#requestFeature getWindow().requestFeature()}.
+ *
+ * @param featureId The desired feature as defined in
+ * {@link android.view.Window}.
+ * @return Returns true if the requested feature is supported and now
+ * enabled.
+ *
+ * @see android.view.Window#requestFeature
+ */
+ public final boolean requestWindowFeature(int featureId) {
+ return getWindow().requestFeature(featureId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableResource}.
+ */
+ public final void setFeatureDrawableResource(int featureId, @DrawableRes int resId) {
+ getWindow().setFeatureDrawableResource(featureId, resId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableUri}.
+ */
+ public final void setFeatureDrawableUri(int featureId, Uri uri) {
+ getWindow().setFeatureDrawableUri(featureId, uri);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawable(int, Drawable)}.
+ */
+ public final void setFeatureDrawable(int featureId, Drawable drawable) {
+ getWindow().setFeatureDrawable(featureId, drawable);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableAlpha}.
+ */
+ public final void setFeatureDrawableAlpha(int featureId, int alpha) {
+ getWindow().setFeatureDrawableAlpha(featureId, alpha);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#getLayoutInflater}.
+ */
+ @NonNull
+ public LayoutInflater getLayoutInflater() {
+ return getWindow().getLayoutInflater();
+ }
+
+ /**
+ * Returns a {@link MenuInflater} with this context.
+ */
+ @NonNull
+ public MenuInflater getMenuInflater() {
+ // Make sure that action views can get an appropriate theme.
+ if (mMenuInflater == null) {
+ initWindowDecorActionBar();
+ if (mActionBar != null) {
+ mMenuInflater = new MenuInflater(mActionBar.getThemedContext(), this);
+ } else {
+ mMenuInflater = new MenuInflater(this);
+ }
+ }
+ return mMenuInflater;
+ }
+
+ @Override
+ public void setTheme(int resid) {
+ super.setTheme(resid);
+ mWindow.setTheme(resid);
+ }
+
+ @Override
+ protected void onApplyThemeResource(Resources.Theme theme, @StyleRes int resid,
+ boolean first) {
+ if (mParent == null) {
+ super.onApplyThemeResource(theme, resid, first);
+ } else {
+ try {
+ theme.setTo(mParent.getTheme());
+ } catch (Exception e) {
+ // Empty
+ }
+ theme.applyStyle(resid, false);
+ }
+
+ // Get the primary color and update the TaskDescription for this activity
+ TypedArray a = theme.obtainStyledAttributes(
+ com.android.internal.R.styleable.ActivityTaskDescription);
+ if (mTaskDescription.getPrimaryColor() == 0) {
+ int colorPrimary = a.getColor(
+ com.android.internal.R.styleable.ActivityTaskDescription_colorPrimary, 0);
+ if (colorPrimary != 0 && Color.alpha(colorPrimary) == 0xFF) {
+ mTaskDescription.setPrimaryColor(colorPrimary);
+ }
+ }
+
+ int colorBackground = a.getColor(
+ com.android.internal.R.styleable.ActivityTaskDescription_colorBackground, 0);
+ if (colorBackground != 0 && Color.alpha(colorBackground) == 0xFF) {
+ mTaskDescription.setBackgroundColor(colorBackground);
+ }
+
+ final int statusBarColor = a.getColor(
+ com.android.internal.R.styleable.ActivityTaskDescription_statusBarColor, 0);
+ if (statusBarColor != 0) {
+ mTaskDescription.setStatusBarColor(statusBarColor);
+ }
+
+ final int navigationBarColor = a.getColor(
+ com.android.internal.R.styleable.ActivityTaskDescription_navigationBarColor, 0);
+ if (navigationBarColor != 0) {
+ mTaskDescription.setNavigationBarColor(navigationBarColor);
+ }
+
+ a.recycle();
+ setTaskDescription(mTaskDescription);
+ }
+
+ /**
+ * Requests permissions to be granted to this application. These permissions
+ * must be requested in your manifest, they should not be granted to your app,
+ * and they should have protection level {@link android.content.pm.PermissionInfo
+ * #PROTECTION_DANGEROUS dangerous}, regardless whether they are declared by
+ * the platform or a third-party app.
+ * <p>
+ * Normal permissions {@link android.content.pm.PermissionInfo#PROTECTION_NORMAL}
+ * are granted at install time if requested in the manifest. Signature permissions
+ * {@link android.content.pm.PermissionInfo#PROTECTION_SIGNATURE} are granted at
+ * install time if requested in the manifest and the signature of your app matches
+ * the signature of the app declaring the permissions.
+ * </p>
+ * <p>
+ * If your app does not have the requested permissions the user will be presented
+ * with UI for accepting them. After the user has accepted or rejected the
+ * requested permissions you will receive a callback on {@link
+ * #onRequestPermissionsResult(int, String[], int[])} reporting whether the
+ * permissions were granted or not.
+ * </p>
+ * <p>
+ * Note that requesting a permission does not guarantee it will be granted and
+ * your app should be able to run without having this permission.
+ * </p>
+ * <p>
+ * This method may start an activity allowing the user to choose which permissions
+ * to grant and which to reject. Hence, you should be prepared that your activity
+ * may be paused and resumed. Further, granting some permissions may require
+ * a restart of you application. In such a case, the system will recreate the
+ * activity stack before delivering the result to {@link
+ * #onRequestPermissionsResult(int, String[], int[])}.
+ * </p>
+ * <p>
+ * When checking whether you have a permission you should use {@link
+ * #checkSelfPermission(String)}.
+ * </p>
+ * <p>
+ * Calling this API for permissions already granted to your app would show UI
+ * to the user to decide whether the app can still hold these permissions. This
+ * can be useful if the way your app uses data guarded by the permissions
+ * changes significantly.
+ * </p>
+ * <p>
+ * You cannot request a permission if your activity sets {@link
+ * android.R.styleable#AndroidManifestActivity_noHistory noHistory} to
+ * <code>true</code> because in this case the activity would not receive
+ * result callbacks including {@link #onRequestPermissionsResult(int, String[], int[])}.
+ * </p>
+ * <p>
+ * The <a href="http://developer.android.com/samples/RuntimePermissions/index.html">
+ * RuntimePermissions</a> sample app demonstrates how to use this method to
+ * request permissions at run time.
+ * </p>
+ *
+ * @param permissions The requested permissions. Must me non-null and not empty.
+ * @param requestCode Application specific request code to match with a result
+ * reported to {@link #onRequestPermissionsResult(int, String[], int[])}.
+ * Should be >= 0.
+ *
+ * @throws IllegalArgumentException if requestCode is negative.
+ *
+ * @see #onRequestPermissionsResult(int, String[], int[])
+ * @see #checkSelfPermission(String)
+ * @see #shouldShowRequestPermissionRationale(String)
+ */
+ public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
+ if (requestCode < 0) {
+ throw new IllegalArgumentException("requestCode should be >= 0");
+ }
+ if (mHasCurrentPermissionsRequest) {
+ Log.w(TAG, "Can reqeust only one set of permissions at a time");
+ // Dispatch the callback with empty arrays which means a cancellation.
+ onRequestPermissionsResult(requestCode, new String[0], new int[0]);
+ return;
+ }
+ Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
+ startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
+ mHasCurrentPermissionsRequest = true;
+ }
+
+ /**
+ * Callback for the result from requesting permissions. This method
+ * is invoked for every call on {@link #requestPermissions(String[], int)}.
+ * <p>
+ * <strong>Note:</strong> It is possible that the permissions request interaction
+ * with the user is interrupted. In this case you will receive empty permissions
+ * and results arrays which should be treated as a cancellation.
+ * </p>
+ *
+ * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}.
+ * @param permissions The requested permissions. Never null.
+ * @param grantResults The grant results for the corresponding permissions
+ * which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}
+ * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.
+ *
+ * @see #requestPermissions(String[], int)
+ */
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ /* callback - no nothing */
+ }
+
+ /**
+ * Gets whether you should show UI with rationale for requesting a permission.
+ * You should do this only if you do not have the permission and the context in
+ * which the permission is requested does not clearly communicate to the user
+ * what would be the benefit from granting this permission.
+ * <p>
+ * For example, if you write a camera app, requesting the camera permission
+ * would be expected by the user and no rationale for why it is requested is
+ * needed. If however, the app needs location for tagging photos then a non-tech
+ * savvy user may wonder how location is related to taking photos. In this case
+ * you may choose to show UI with rationale of requesting this permission.
+ * </p>
+ *
+ * @param permission A permission your app wants to request.
+ * @return Whether you can show permission rationale UI.
+ *
+ * @see #checkSelfPermission(String)
+ * @see #requestPermissions(String[], int)
+ * @see #onRequestPermissionsResult(int, String[], int[])
+ */
+ public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
+ return getPackageManager().shouldShowRequestPermissionRationale(permission);
+ }
+
+ /**
+ * Same as calling {@link #startActivityForResult(Intent, int, Bundle)}
+ * with no options.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ */
+ public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
+ startActivityForResult(intent, requestCode, null);
+ }
+
+ /**
+ * Launch an activity for which you would like a result when it finished.
+ * When this activity exits, your
+ * onActivityResult() method will be called with the given requestCode.
+ * Using a negative requestCode is the same as calling
+ * {@link #startActivity} (the activity is not launched as a sub-activity).
+ *
+ * <p>Note that this method should only be used with Intent protocols
+ * that are defined to return a result. In other protocols (such as
+ * {@link Intent#ACTION_MAIN} or {@link Intent#ACTION_VIEW}), you may
+ * not get the result when you expect. For example, if the activity you
+ * are launching uses {@link Intent#FLAG_ACTIVITY_NEW_TASK}, it will not
+ * run in your task and thus you will immediately receive a cancel result.
+ *
+ * <p>As a special case, if you call startActivityForResult() with a requestCode
+ * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your
+ * activity, then your window will not be displayed until a result is
+ * returned back from the started activity. This is to avoid visible
+ * flickering when redirecting to another activity.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ */
+ public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
+ @Nullable Bundle options) {
+ if (mParent == null) {
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, this,
+ intent, requestCode, options);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, mEmbeddedID, requestCode, ar.getResultCode(),
+ ar.getResultData());
+ }
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+
+ cancelInputsAndStartExitTransition(options);
+ // TODO Consider clearing/flushing other event sources and events for child windows.
+ } else {
+ if (options != null) {
+ mParent.startActivityFromChild(this, intent, requestCode, options);
+ } else {
+ // Note we want to go through this method for compatibility with
+ // existing applications that may have overridden it.
+ mParent.startActivityFromChild(this, intent, requestCode);
+ }
+ }
+ }
+
+ /**
+ * Cancels pending inputs and if an Activity Transition is to be run, starts the transition.
+ *
+ * @param options The ActivityOptions bundle used to start an Activity.
+ */
+ private void cancelInputsAndStartExitTransition(Bundle options) {
+ final View decor = mWindow != null ? mWindow.peekDecorView() : null;
+ if (decor != null) {
+ decor.cancelPendingInputEvents();
+ }
+ if (options != null && !isTopOfTask()) {
+ mActivityTransitionState.startExitOutTransition(this, options);
+ }
+ }
+
+ /**
+ * Returns whether there are any activity transitions currently running on this
+ * activity. A return value of {@code true} can mean that either an enter or
+ * exit transition is running, including whether the background of the activity
+ * is animating as a part of that transition.
+ *
+ * @return true if a transition is currently running on this activity, false otherwise.
+ */
+ public boolean isActivityTransitionRunning() {
+ return mActivityTransitionState.isTransitionRunning();
+ }
+
+ private Bundle transferSpringboardActivityOptions(Bundle options) {
+ if (options == null && (mWindow != null && !mWindow.isActive())) {
+ final ActivityOptions activityOptions = getActivityOptions();
+ if (activityOptions != null &&
+ activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
+ return activityOptions.toBundle();
+ }
+ }
+ return options;
+ }
+
+ /**
+ * @hide Implement to provide correct calling token.
+ */
+ public void startActivityForResultAsUser(Intent intent, int requestCode, UserHandle user) {
+ startActivityForResultAsUser(intent, requestCode, null, user);
+ }
+
+ /**
+ * @hide Implement to provide correct calling token.
+ */
+ public void startActivityForResultAsUser(Intent intent, int requestCode,
+ @Nullable Bundle options, UserHandle user) {
+ startActivityForResultAsUser(intent, mEmbeddedID, requestCode, options, user);
+ }
+
+ /**
+ * @hide Implement to provide correct calling token.
+ */
+ public void startActivityForResultAsUser(Intent intent, String resultWho, int requestCode,
+ @Nullable Bundle options, UserHandle user) {
+ if (mParent != null) {
+ throw new RuntimeException("Can't be called from a child");
+ }
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, resultWho, intent, requestCode,
+ options, user);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData());
+ }
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+
+ cancelInputsAndStartExitTransition(options);
+ }
+
+ /**
+ * @hide Implement to provide correct calling token.
+ */
+ public void startActivityAsUser(Intent intent, UserHandle user) {
+ startActivityAsUser(intent, null, user);
+ }
+
+ /**
+ * @hide Implement to provide correct calling token.
+ */
+ public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
+ if (mParent != null) {
+ throw new RuntimeException("Can't be called from a child");
+ }
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, mEmbeddedID,
+ intent, -1, options, user);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, mEmbeddedID, -1, ar.getResultCode(),
+ ar.getResultData());
+ }
+ cancelInputsAndStartExitTransition(options);
+ }
+
+ /**
+ * Start a new activity as if it was started by the activity that started our
+ * current activity. This is for the resolver and chooser activities, which operate
+ * as intermediaries that dispatch their intent to the target the user selects -- to
+ * do this, they must perform all security checks including permission grants as if
+ * their launch had come from the original activity.
+ * @param intent The Intent to start.
+ * @param options ActivityOptions or null.
+ * @param ignoreTargetSecurity If true, the activity manager will not check whether the
+ * caller it is doing the start is, is actually allowed to start the target activity.
+ * If you set this to true, you must set an explicit component in the Intent and do any
+ * appropriate security checks yourself.
+ * @param userId The user the new activity should run as.
+ * @hide
+ */
+ public void startActivityAsCaller(Intent intent, @Nullable Bundle options,
+ boolean ignoreTargetSecurity, int userId) {
+ if (mParent != null) {
+ throw new RuntimeException("Can't be called from a child");
+ }
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivityAsCaller(
+ this, mMainThread.getApplicationThread(), mToken, this,
+ intent, -1, options, ignoreTargetSecurity, userId);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, mEmbeddedID, -1, ar.getResultCode(),
+ ar.getResultData());
+ }
+ cancelInputsAndStartExitTransition(options);
+ }
+
+ /**
+ * Same as calling {@link #startIntentSenderForResult(IntentSender, int,
+ * Intent, int, int, int, Bundle)} with no options.
+ *
+ * @param intent The IntentSender to launch.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits.
+ * @param fillInIntent If non-null, this will be provided as the
+ * intent parameter to {@link IntentSender#sendIntent}.
+ * @param flagsMask Intent flags in the original IntentSender that you
+ * would like to change.
+ * @param flagsValues Desired values for any bits set in
+ * <var>flagsMask</var>
+ * @param extraFlags Always set to 0.
+ */
+ public void startIntentSenderForResult(IntentSender intent, int requestCode,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)
+ throws IntentSender.SendIntentException {
+ startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask,
+ flagsValues, extraFlags, null);
+ }
+
+ /**
+ * Like {@link #startActivityForResult(Intent, int)}, but allowing you
+ * to use a IntentSender to describe the activity to be started. If
+ * the IntentSender is for an activity, that activity will be started
+ * as if you had called the regular {@link #startActivityForResult(Intent, int)}
+ * here; otherwise, its associated action will be executed (such as
+ * sending a broadcast) as if you had called
+ * {@link IntentSender#sendIntent IntentSender.sendIntent} on it.
+ *
+ * @param intent The IntentSender to launch.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits.
+ * @param fillInIntent If non-null, this will be provided as the
+ * intent parameter to {@link IntentSender#sendIntent}.
+ * @param flagsMask Intent flags in the original IntentSender that you
+ * would like to change.
+ * @param flagsValues Desired values for any bits set in
+ * <var>flagsMask</var>
+ * @param extraFlags Always set to 0.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details. If options
+ * have also been supplied by the IntentSender, options given here will
+ * override any that conflict with those given by the IntentSender.
+ */
+ public void startIntentSenderForResult(IntentSender intent, int requestCode,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
+ Bundle options) throws IntentSender.SendIntentException {
+ if (mParent == null) {
+ startIntentSenderForResultInner(intent, mEmbeddedID, requestCode, fillInIntent,
+ flagsMask, flagsValues, options);
+ } else if (options != null) {
+ mParent.startIntentSenderFromChild(this, intent, requestCode,
+ fillInIntent, flagsMask, flagsValues, extraFlags, options);
+ } else {
+ // Note we want to go through this call for compatibility with
+ // existing applications that may have overridden the method.
+ mParent.startIntentSenderFromChild(this, intent, requestCode,
+ fillInIntent, flagsMask, flagsValues, extraFlags);
+ }
+ }
+
+ private void startIntentSenderForResultInner(IntentSender intent, String who, int requestCode,
+ Intent fillInIntent, int flagsMask, int flagsValues,
+ Bundle options)
+ throws IntentSender.SendIntentException {
+ try {
+ String resolvedType = null;
+ if (fillInIntent != null) {
+ fillInIntent.migrateExtraStreamToClipData();
+ fillInIntent.prepareToLeaveProcess(this);
+ resolvedType = fillInIntent.resolveTypeIfNeeded(getContentResolver());
+ }
+ int result = ActivityManager.getService()
+ .startActivityIntentSender(mMainThread.getApplicationThread(),
+ intent != null ? intent.getTarget() : null,
+ intent != null ? intent.getWhitelistToken() : null,
+ fillInIntent, resolvedType, mToken, who,
+ requestCode, flagsMask, flagsValues, options);
+ if (result == ActivityManager.START_CANCELED) {
+ throw new IntentSender.SendIntentException();
+ }
+ Instrumentation.checkStartActivityResult(result, null);
+ } catch (RemoteException e) {
+ }
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+ }
+
+ /**
+ * Same as {@link #startActivity(Intent, Bundle)} with no options
+ * specified.
+ *
+ * @param intent The intent to start.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity(Intent, Bundle)
+ * @see #startActivityForResult
+ */
+ @Override
+ public void startActivity(Intent intent) {
+ this.startActivity(intent, null);
+ }
+
+ /**
+ * Launch a new activity. You will not receive any information about when
+ * the activity exits. This implementation overrides the base version,
+ * providing information about
+ * the activity performing the launch. Because of this additional
+ * information, the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag is not
+ * required; if not specified, the new activity will be added to the
+ * task of the caller.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intent The intent to start.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity(Intent)
+ * @see #startActivityForResult
+ */
+ @Override
+ public void startActivity(Intent intent, @Nullable Bundle options) {
+ if (options != null) {
+ startActivityForResult(intent, -1, options);
+ } else {
+ // Note we want to go through this call for compatibility with
+ // applications that may have overridden the method.
+ startActivityForResult(intent, -1);
+ }
+ }
+
+ /**
+ * Same as {@link #startActivities(Intent[], Bundle)} with no options
+ * specified.
+ *
+ * @param intents The intents to start.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivities(Intent[], Bundle)
+ * @see #startActivityForResult
+ */
+ @Override
+ public void startActivities(Intent[] intents) {
+ startActivities(intents, null);
+ }
+
+ /**
+ * Launch a new activity. You will not receive any information about when
+ * the activity exits. This implementation overrides the base version,
+ * providing information about
+ * the activity performing the launch. Because of this additional
+ * information, the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag is not
+ * required; if not specified, the new activity will be added to the
+ * task of the caller.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intents The intents to start.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivities(Intent[])
+ * @see #startActivityForResult
+ */
+ @Override
+ public void startActivities(Intent[] intents, @Nullable Bundle options) {
+ mInstrumentation.execStartActivities(this, mMainThread.getApplicationThread(),
+ mToken, this, intents, options);
+ }
+
+ /**
+ * Same as calling {@link #startIntentSender(IntentSender, Intent, int, int, int, Bundle)}
+ * with no options.
+ *
+ * @param intent The IntentSender to launch.
+ * @param fillInIntent If non-null, this will be provided as the
+ * intent parameter to {@link IntentSender#sendIntent}.
+ * @param flagsMask Intent flags in the original IntentSender that you
+ * would like to change.
+ * @param flagsValues Desired values for any bits set in
+ * <var>flagsMask</var>
+ * @param extraFlags Always set to 0.
+ */
+ public void startIntentSender(IntentSender intent,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)
+ throws IntentSender.SendIntentException {
+ startIntentSender(intent, fillInIntent, flagsMask, flagsValues,
+ extraFlags, null);
+ }
+
+ /**
+ * Like {@link #startActivity(Intent, Bundle)}, but taking a IntentSender
+ * to start; see
+ * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int, Bundle)}
+ * for more information.
+ *
+ * @param intent The IntentSender to launch.
+ * @param fillInIntent If non-null, this will be provided as the
+ * intent parameter to {@link IntentSender#sendIntent}.
+ * @param flagsMask Intent flags in the original IntentSender that you
+ * would like to change.
+ * @param flagsValues Desired values for any bits set in
+ * <var>flagsMask</var>
+ * @param extraFlags Always set to 0.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details. If options
+ * have also been supplied by the IntentSender, options given here will
+ * override any that conflict with those given by the IntentSender.
+ */
+ public void startIntentSender(IntentSender intent,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
+ Bundle options) throws IntentSender.SendIntentException {
+ if (options != null) {
+ startIntentSenderForResult(intent, -1, fillInIntent, flagsMask,
+ flagsValues, extraFlags, options);
+ } else {
+ // Note we want to go through this call for compatibility with
+ // applications that may have overridden the method.
+ startIntentSenderForResult(intent, -1, fillInIntent, flagsMask,
+ flagsValues, extraFlags);
+ }
+ }
+
+ /**
+ * Same as calling {@link #startActivityIfNeeded(Intent, int, Bundle)}
+ * with no options.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits, as described in
+ * {@link #startActivityForResult}.
+ *
+ * @return If a new activity was launched then true is returned; otherwise
+ * false is returned and you must handle the Intent yourself.
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public boolean startActivityIfNeeded(@RequiresPermission @NonNull Intent intent,
+ int requestCode) {
+ return startActivityIfNeeded(intent, requestCode, null);
+ }
+
+ /**
+ * A special variation to launch an activity only if a new activity
+ * instance is needed to handle the given Intent. In other words, this is
+ * just like {@link #startActivityForResult(Intent, int)} except: if you are
+ * using the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} flag, or
+ * singleTask or singleTop
+ * {@link android.R.styleable#AndroidManifestActivity_launchMode launchMode},
+ * and the activity
+ * that handles <var>intent</var> is the same as your currently running
+ * activity, then a new instance is not needed. In this case, instead of
+ * the normal behavior of calling {@link #onNewIntent} this function will
+ * return and you can handle the Intent yourself.
+ *
+ * <p>This function can only be called from a top-level activity; if it is
+ * called from a child activity, a runtime exception will be thrown.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits, as described in
+ * {@link #startActivityForResult}.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @return If a new activity was launched then true is returned; otherwise
+ * false is returned and you must handle the Intent yourself.
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public boolean startActivityIfNeeded(@RequiresPermission @NonNull Intent intent,
+ int requestCode, @Nullable Bundle options) {
+ if (mParent == null) {
+ int result = ActivityManager.START_RETURN_INTENT_TO_CALLER;
+ try {
+ Uri referrer = onProvideReferrer();
+ if (referrer != null) {
+ intent.putExtra(Intent.EXTRA_REFERRER, referrer);
+ }
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(this);
+ result = ActivityManager.getService()
+ .startActivity(mMainThread.getApplicationThread(), getBasePackageName(),
+ intent, intent.resolveTypeIfNeeded(getContentResolver()), mToken,
+ mEmbeddedID, requestCode, ActivityManager.START_FLAG_ONLY_IF_NEEDED,
+ null, options);
+ } catch (RemoteException e) {
+ // Empty
+ }
+
+ Instrumentation.checkStartActivityResult(result, intent);
+
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+ return result != ActivityManager.START_RETURN_INTENT_TO_CALLER;
+ }
+
+ throw new UnsupportedOperationException(
+ "startActivityIfNeeded can only be called from a top-level activity");
+ }
+
+ /**
+ * Same as calling {@link #startNextMatchingActivity(Intent, Bundle)} with
+ * no options.
+ *
+ * @param intent The intent to dispatch to the next activity. For
+ * correct behavior, this must be the same as the Intent that started
+ * your own activity; the only changes you can make are to the extras
+ * inside of it.
+ *
+ * @return Returns a boolean indicating whether there was another Activity
+ * to start: true if there was a next activity to start, false if there
+ * wasn't. In general, if true is returned you will then want to call
+ * finish() on yourself.
+ */
+ public boolean startNextMatchingActivity(@RequiresPermission @NonNull Intent intent) {
+ return startNextMatchingActivity(intent, null);
+ }
+
+ /**
+ * Special version of starting an activity, for use when you are replacing
+ * other activity components. You can use this to hand the Intent off
+ * to the next Activity that can handle it. You typically call this in
+ * {@link #onCreate} with the Intent returned by {@link #getIntent}.
+ *
+ * @param intent The intent to dispatch to the next activity. For
+ * correct behavior, this must be the same as the Intent that started
+ * your own activity; the only changes you can make are to the extras
+ * inside of it.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @return Returns a boolean indicating whether there was another Activity
+ * to start: true if there was a next activity to start, false if there
+ * wasn't. In general, if true is returned you will then want to call
+ * finish() on yourself.
+ */
+ public boolean startNextMatchingActivity(@RequiresPermission @NonNull Intent intent,
+ @Nullable Bundle options) {
+ if (mParent == null) {
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(this);
+ return ActivityManager.getService()
+ .startNextMatchingActivity(mToken, intent, options);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ throw new UnsupportedOperationException(
+ "startNextMatchingActivity can only be called from a top-level activity");
+ }
+
+ /**
+ * Same as calling {@link #startActivityFromChild(Activity, Intent, int, Bundle)}
+ * with no options.
+ *
+ * @param child The activity making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
+ int requestCode) {
+ startActivityFromChild(child, intent, requestCode, null);
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * {@link #startActivity} or {@link #startActivityForResult} method.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param child The activity making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
+ int requestCode, @Nullable Bundle options) {
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, child,
+ intent, requestCode, options);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, child.mEmbeddedID, requestCode,
+ ar.getResultCode(), ar.getResultData());
+ }
+ cancelInputsAndStartExitTransition(options);
+ }
+
+ /**
+ * Same as calling {@link #startActivityFromFragment(Fragment, Intent, int, Bundle)}
+ * with no options.
+ *
+ * @param fragment The fragment making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Fragment#startActivity
+ * @see Fragment#startActivityForResult
+ */
+ public void startActivityFromFragment(@NonNull Fragment fragment,
+ @RequiresPermission Intent intent, int requestCode) {
+ startActivityFromFragment(fragment, intent, requestCode, null);
+ }
+
+ /**
+ * This is called when a Fragment in this activity calls its
+ * {@link Fragment#startActivity} or {@link Fragment#startActivityForResult}
+ * method.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param fragment The fragment making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Fragment#startActivity
+ * @see Fragment#startActivityForResult
+ */
+ public void startActivityFromFragment(@NonNull Fragment fragment,
+ @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
+ startActivityForResult(fragment.mWho, intent, requestCode, options);
+ }
+
+ /**
+ * @hide
+ */
+ public void startActivityAsUserFromFragment(@NonNull Fragment fragment,
+ @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options,
+ UserHandle user) {
+ startActivityForResultAsUser(intent, fragment.mWho, requestCode, options, user);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void startActivityForResult(
+ String who, Intent intent, int requestCode, @Nullable Bundle options) {
+ Uri referrer = onProvideReferrer();
+ if (referrer != null) {
+ intent.putExtra(Intent.EXTRA_REFERRER, referrer);
+ }
+ options = transferSpringboardActivityOptions(options);
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, who,
+ intent, requestCode, options);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, who, requestCode,
+ ar.getResultCode(), ar.getResultData());
+ }
+ cancelInputsAndStartExitTransition(options);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean canStartActivityForResult() {
+ return true;
+ }
+
+ /**
+ * Same as calling {@link #startIntentSenderFromChild(Activity, IntentSender,
+ * int, Intent, int, int, int, Bundle)} with no options.
+ */
+ public void startIntentSenderFromChild(Activity child, IntentSender intent,
+ int requestCode, Intent fillInIntent, int flagsMask, int flagsValues,
+ int extraFlags)
+ throws IntentSender.SendIntentException {
+ startIntentSenderFromChild(child, intent, requestCode, fillInIntent,
+ flagsMask, flagsValues, extraFlags, null);
+ }
+
+ /**
+ * Like {@link #startActivityFromChild(Activity, Intent, int)}, but
+ * taking a IntentSender; see
+ * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}
+ * for more information.
+ */
+ public void startIntentSenderFromChild(Activity child, IntentSender intent,
+ int requestCode, Intent fillInIntent, int flagsMask, int flagsValues,
+ int extraFlags, @Nullable Bundle options)
+ throws IntentSender.SendIntentException {
+ startIntentSenderForResultInner(intent, child.mEmbeddedID, requestCode, fillInIntent,
+ flagsMask, flagsValues, options);
+ }
+
+ /**
+ * Like {@link #startIntentSenderFromChild}, but taking a Fragment; see
+ * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}
+ * for more information.
+ *
+ * @hide
+ */
+ public void startIntentSenderFromChildFragment(Fragment child, IntentSender intent,
+ int requestCode, Intent fillInIntent, int flagsMask, int flagsValues,
+ int extraFlags, @Nullable Bundle options)
+ throws IntentSender.SendIntentException {
+ startIntentSenderForResultInner(intent, child.mWho, requestCode, fillInIntent,
+ flagsMask, flagsValues, options);
+ }
+
+ /**
+ * Call immediately after one of the flavors of {@link #startActivity(Intent)}
+ * or {@link #finish} to specify an explicit transition animation to
+ * perform next.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN} an alternative
+ * to using this with starting activities is to supply the desired animation
+ * information through a {@link ActivityOptions} bundle to
+ * {@link #startActivity(Intent, Bundle)} or a related function. This allows
+ * you to specify a custom animation even when starting an activity from
+ * outside the context of the current top activity.
+ *
+ * @param enterAnim A resource ID of the animation resource to use for
+ * the incoming activity. Use 0 for no animation.
+ * @param exitAnim A resource ID of the animation resource to use for
+ * the outgoing activity. Use 0 for no animation.
+ */
+ public void overridePendingTransition(int enterAnim, int exitAnim) {
+ try {
+ ActivityManager.getService().overridePendingTransition(
+ mToken, getPackageName(), enterAnim, exitAnim);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Call this to set the result that your activity will return to its
+ * caller.
+ *
+ * @param resultCode The result code to propagate back to the originating
+ * activity, often RESULT_CANCELED or RESULT_OK
+ *
+ * @see #RESULT_CANCELED
+ * @see #RESULT_OK
+ * @see #RESULT_FIRST_USER
+ * @see #setResult(int, Intent)
+ */
+ public final void setResult(int resultCode) {
+ synchronized (this) {
+ mResultCode = resultCode;
+ mResultData = null;
+ }
+ }
+
+ /**
+ * Call this to set the result that your activity will return to its
+ * caller.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#GINGERBREAD}, the Intent
+ * you supply here can have {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} and/or {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION} set. This will grant the
+ * Activity receiving the result access to the specific URIs in the Intent.
+ * Access will remain until the Activity has finished (it will remain across the hosting
+ * process being killed and other temporary destruction) and will be added
+ * to any existing set of URI permissions it already holds.
+ *
+ * @param resultCode The result code to propagate back to the originating
+ * activity, often RESULT_CANCELED or RESULT_OK
+ * @param data The data to propagate back to the originating activity.
+ *
+ * @see #RESULT_CANCELED
+ * @see #RESULT_OK
+ * @see #RESULT_FIRST_USER
+ * @see #setResult(int)
+ */
+ public final void setResult(int resultCode, Intent data) {
+ synchronized (this) {
+ mResultCode = resultCode;
+ mResultData = data;
+ }
+ }
+
+ /**
+ * Return information about who launched this activity. If the launching Intent
+ * contains an {@link android.content.Intent#EXTRA_REFERRER Intent.EXTRA_REFERRER},
+ * that will be returned as-is; otherwise, if known, an
+ * {@link Intent#URI_ANDROID_APP_SCHEME android-app:} referrer URI containing the
+ * package name that started the Intent will be returned. This may return null if no
+ * referrer can be identified -- it is neither explicitly specified, nor is it known which
+ * application package was involved.
+ *
+ * <p>If called while inside the handling of {@link #onNewIntent}, this function will
+ * return the referrer that submitted that new intent to the activity. Otherwise, it
+ * always returns the referrer of the original Intent.</p>
+ *
+ * <p>Note that this is <em>not</em> a security feature -- you can not trust the
+ * referrer information, applications can spoof it.</p>
+ */
+ @Nullable
+ public Uri getReferrer() {
+ Intent intent = getIntent();
+ try {
+ Uri referrer = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
+ if (referrer != null) {
+ return referrer;
+ }
+ String referrerName = intent.getStringExtra(Intent.EXTRA_REFERRER_NAME);
+ if (referrerName != null) {
+ return Uri.parse(referrerName);
+ }
+ } catch (BadParcelableException e) {
+ Log.w(TAG, "Cannot read referrer from intent;"
+ + " intent extras contain unknown custom Parcelable objects");
+ }
+ if (mReferrer != null) {
+ return new Uri.Builder().scheme("android-app").authority(mReferrer).build();
+ }
+ return null;
+ }
+
+ /**
+ * Override to generate the desired referrer for the content currently being shown
+ * by the app. The default implementation returns null, meaning the referrer will simply
+ * be the android-app: of the package name of this activity. Return a non-null Uri to
+ * have that supplied as the {@link Intent#EXTRA_REFERRER} of any activities started from it.
+ */
+ public Uri onProvideReferrer() {
+ return null;
+ }
+
+ /**
+ * Return the name of the package that invoked this activity. This is who
+ * the data in {@link #setResult setResult()} will be sent to. You can
+ * use this information to validate that the recipient is allowed to
+ * receive the data.
+ *
+ * <p class="note">Note: if the calling activity is not expecting a result (that is it
+ * did not use the {@link #startActivityForResult}
+ * form that includes a request code), then the calling package will be
+ * null.</p>
+ *
+ * <p class="note">Note: prior to {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+ * the result from this method was unstable. If the process hosting the calling
+ * package was no longer running, it would return null instead of the proper package
+ * name. You can use {@link #getCallingActivity()} and retrieve the package name
+ * from that instead.</p>
+ *
+ * @return The package of the activity that will receive your
+ * reply, or null if none.
+ */
+ @Nullable
+ public String getCallingPackage() {
+ try {
+ return ActivityManager.getService().getCallingPackage(mToken);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return the name of the activity that invoked this activity. This is
+ * who the data in {@link #setResult setResult()} will be sent to. You
+ * can use this information to validate that the recipient is allowed to
+ * receive the data.
+ *
+ * <p class="note">Note: if the calling activity is not expecting a result (that is it
+ * did not use the {@link #startActivityForResult}
+ * form that includes a request code), then the calling package will be
+ * null.
+ *
+ * @return The ComponentName of the activity that will receive your
+ * reply, or null if none.
+ */
+ @Nullable
+ public ComponentName getCallingActivity() {
+ try {
+ return ActivityManager.getService().getCallingActivity(mToken);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Control whether this activity's main window is visible. This is intended
+ * only for the special case of an activity that is not going to show a
+ * UI itself, but can't just finish prior to onResume() because it needs
+ * to wait for a service binding or such. Setting this to false allows
+ * you to prevent your UI from being shown during that time.
+ *
+ * <p>The default value for this is taken from the
+ * {@link android.R.attr#windowNoDisplay} attribute of the activity's theme.
+ */
+ public void setVisible(boolean visible) {
+ if (mVisibleFromClient != visible) {
+ mVisibleFromClient = visible;
+ if (mVisibleFromServer) {
+ if (visible) makeVisible();
+ else mDecor.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ void makeVisible() {
+ if (!mWindowAdded) {
+ ViewManager wm = getWindowManager();
+ wm.addView(mDecor, getWindow().getAttributes());
+ mWindowAdded = true;
+ }
+ mDecor.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Check to see whether this activity is in the process of finishing,
+ * either because you called {@link #finish} on it or someone else
+ * has requested that it finished. This is often used in
+ * {@link #onPause} to determine whether the activity is simply pausing or
+ * completely finishing.
+ *
+ * @return If the activity is finishing, returns true; else returns false.
+ *
+ * @see #finish
+ */
+ public boolean isFinishing() {
+ return mFinished;
+ }
+
+ /**
+ * Returns true if the final {@link #onDestroy()} call has been made
+ * on the Activity, so this instance is now dead.
+ */
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * Check to see whether this activity is in the process of being destroyed in order to be
+ * recreated with a new configuration. This is often used in
+ * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed
+ * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}.
+ *
+ * @return If the activity is being torn down in order to be recreated with a new configuration,
+ * returns true; else returns false.
+ */
+ public boolean isChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ /**
+ * Cause this Activity to be recreated with a new instance. This results
+ * in essentially the same flow as when the Activity is created due to
+ * a configuration change -- the current instance will go through its
+ * lifecycle to {@link #onDestroy} and a new instance then created after it.
+ */
+ public void recreate() {
+ if (mParent != null) {
+ throw new IllegalStateException("Can only be called on top-level activity");
+ }
+ if (Looper.myLooper() != mMainThread.getLooper()) {
+ throw new IllegalStateException("Must be called from main thread");
+ }
+ try {
+ ActivityManager.getService().requestActivityRelaunch(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Finishes the current activity and specifies whether to remove the task associated with this
+ * activity.
+ */
+ private void finish(int finishTask) {
+ if (mParent == null) {
+ int resultCode;
+ Intent resultData;
+ synchronized (this) {
+ resultCode = mResultCode;
+ resultData = mResultData;
+ }
+ if (false) Log.v(TAG, "Finishing self: token=" + mToken);
+ try {
+ if (resultData != null) {
+ resultData.prepareToLeaveProcess(this);
+ }
+ if (ActivityManager.getService()
+ .finishActivity(mToken, resultCode, resultData, finishTask)) {
+ mFinished = true;
+ }
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.finishFromChild(this);
+ }
+
+ // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must
+ // be restored now.
+ if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) {
+ getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE,
+ mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
+ }
+ }
+
+ /**
+ * Call this when your activity is done and should be closed. The
+ * ActivityResult is propagated back to whoever launched you via
+ * onActivityResult().
+ */
+ public void finish() {
+ finish(DONT_FINISH_TASK_WITH_ACTIVITY);
+ }
+
+ /**
+ * Finish this activity as well as all activities immediately below it
+ * in the current task that have the same affinity. This is typically
+ * used when an application can be launched on to another task (such as
+ * from an ACTION_VIEW of a content type it understands) and the user
+ * has used the up navigation to switch out of the current task and in
+ * to its own task. In this case, if the user has navigated down into
+ * any other activities of the second application, all of those should
+ * be removed from the original task as part of the task switch.
+ *
+ * <p>Note that this finish does <em>not</em> allow you to deliver results
+ * to the previous activity, and an exception will be thrown if you are trying
+ * to do so.</p>
+ */
+ public void finishAffinity() {
+ if (mParent != null) {
+ throw new IllegalStateException("Can not be called from an embedded activity");
+ }
+ if (mResultCode != RESULT_CANCELED || mResultData != null) {
+ throw new IllegalStateException("Can not be called to deliver a result");
+ }
+ try {
+ if (ActivityManager.getService().finishActivityAffinity(mToken)) {
+ mFinished = true;
+ }
+ } catch (RemoteException e) {
+ // Empty
+ }
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * {@link #finish} method. The default implementation simply calls
+ * finish() on this activity (the parent), finishing the entire group.
+ *
+ * @param child The activity making the call.
+ *
+ * @see #finish
+ */
+ public void finishFromChild(Activity child) {
+ finish();
+ }
+
+ /**
+ * Reverses the Activity Scene entry Transition and triggers the calling Activity
+ * to reverse its exit Transition. When the exit Transition completes,
+ * {@link #finish()} is called. If no entry Transition was used, finish() is called
+ * immediately and the Activity exit Transition is run.
+ * @see android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, android.util.Pair[])
+ */
+ public void finishAfterTransition() {
+ if (!mActivityTransitionState.startExitBackTransition(this)) {
+ finish();
+ }
+ }
+
+ /**
+ * Force finish another activity that you had previously started with
+ * {@link #startActivityForResult}.
+ *
+ * @param requestCode The request code of the activity that you had
+ * given to startActivityForResult(). If there are multiple
+ * activities started with this request code, they
+ * will all be finished.
+ */
+ public void finishActivity(int requestCode) {
+ if (mParent == null) {
+ try {
+ ActivityManager.getService()
+ .finishSubActivity(mToken, mEmbeddedID, requestCode);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.finishActivityFromChild(this, requestCode);
+ }
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * finishActivity().
+ *
+ * @param child The activity making the call.
+ * @param requestCode Request code that had been used to start the
+ * activity.
+ */
+ public void finishActivityFromChild(@NonNull Activity child, int requestCode) {
+ try {
+ ActivityManager.getService()
+ .finishSubActivity(mToken, child.mEmbeddedID, requestCode);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ }
+
+ /**
+ * Call this when your activity is done and should be closed and the task should be completely
+ * removed as a part of finishing the root activity of the task.
+ */
+ public void finishAndRemoveTask() {
+ finish(FINISH_TASK_WITH_ROOT_ACTIVITY);
+ }
+
+ /**
+ * Ask that the local app instance of this activity be released to free up its memory.
+ * This is asking for the activity to be destroyed, but does <b>not</b> finish the activity --
+ * a new instance of the activity will later be re-created if needed due to the user
+ * navigating back to it.
+ *
+ * @return Returns true if the activity was in a state that it has started the process
+ * of destroying its current instance; returns false if for any reason this could not
+ * be done: it is currently visible to the user, it is already being destroyed, it is
+ * being finished, it hasn't yet saved its state, etc.
+ */
+ public boolean releaseInstance() {
+ try {
+ return ActivityManager.getService().releaseActivityInstance(mToken);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ /**
+ * Called when an activity you launched exits, giving you the requestCode
+ * you started it with, the resultCode it returned, and any additional
+ * data from it. The <var>resultCode</var> will be
+ * {@link #RESULT_CANCELED} if the activity explicitly returned that,
+ * didn't return any result, or crashed during its operation.
+ *
+ * <p>You will receive this call immediately before onResume() when your
+ * activity is re-starting.
+ *
+ * <p>This method is never invoked if your activity sets
+ * {@link android.R.styleable#AndroidManifestActivity_noHistory noHistory} to
+ * <code>true</code>.
+ *
+ * @param requestCode The integer request code originally supplied to
+ * startActivityForResult(), allowing you to identify who this
+ * result came from.
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ *
+ * @see #startActivityForResult
+ * @see #createPendingResult
+ * @see #setResult(int)
+ */
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ /**
+ * Called when an activity you launched with an activity transition exposes this
+ * Activity through a returning activity transition, giving you the resultCode
+ * and any additional data from it. This method will only be called if the activity
+ * set a result code other than {@link #RESULT_CANCELED} and it supports activity
+ * transitions with {@link Window#FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * <p>The purpose of this function is to let the called Activity send a hint about
+ * its state so that this underlying Activity can prepare to be exposed. A call to
+ * this method does not guarantee that the called Activity has or will be exiting soon.
+ * It only indicates that it will expose this Activity's Window and it has
+ * some data to pass to prepare it.</p>
+ *
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ */
+ public void onActivityReenter(int resultCode, Intent data) {
+ }
+
+ /**
+ * Create a new PendingIntent object which you can hand to others
+ * for them to use to send result data back to your
+ * {@link #onActivityResult} callback. The created object will be either
+ * one-shot (becoming invalid after a result is sent back) or multiple
+ * (allowing any number of results to be sent through it).
+ *
+ * @param requestCode Private request code for the sender that will be
+ * associated with the result data when it is returned. The sender can not
+ * modify this value, allowing you to identify incoming results.
+ * @param data Default data to supply in the result, which may be modified
+ * by the sender.
+ * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT PendingIntent.FLAG_ONE_SHOT},
+ * {@link PendingIntent#FLAG_NO_CREATE PendingIntent.FLAG_NO_CREATE},
+ * {@link PendingIntent#FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT},
+ * {@link PendingIntent#FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT},
+ * or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if
+ * {@link PendingIntent#FLAG_NO_CREATE PendingIntent.FLAG_NO_CREATE} has been
+ * supplied.
+ *
+ * @see PendingIntent
+ */
+ public PendingIntent createPendingResult(int requestCode, @NonNull Intent data,
+ @PendingIntent.Flags int flags) {
+ String packageName = getPackageName();
+ try {
+ data.prepareToLeaveProcess(this);
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_ACTIVITY_RESULT, packageName,
+ mParent == null ? mToken : mParent.mToken,
+ mEmbeddedID, requestCode, new Intent[] { data }, null, flags, null,
+ UserHandle.myUserId());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return null;
+ }
+
+ /**
+ * Change the desired orientation of this activity. If the activity
+ * is currently in the foreground or otherwise impacting the screen
+ * orientation, the screen will immediately be changed (possibly causing
+ * the activity to be restarted). Otherwise, this will be used the next
+ * time the activity is visible.
+ *
+ * @param requestedOrientation An orientation constant as used in
+ * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
+ */
+ public void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
+ if (mParent == null) {
+ try {
+ ActivityManager.getService().setRequestedOrientation(
+ mToken, requestedOrientation);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.setRequestedOrientation(requestedOrientation);
+ }
+ }
+
+ /**
+ * Return the current requested orientation of the activity. This will
+ * either be the orientation requested in its component's manifest, or
+ * the last requested orientation given to
+ * {@link #setRequestedOrientation(int)}.
+ *
+ * @return Returns an orientation constant as used in
+ * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
+ */
+ @ActivityInfo.ScreenOrientation
+ public int getRequestedOrientation() {
+ if (mParent == null) {
+ try {
+ return ActivityManager.getService()
+ .getRequestedOrientation(mToken);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ return mParent.getRequestedOrientation();
+ }
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+
+ /**
+ * Return the identifier of the task this activity is in. This identifier
+ * will remain the same for the lifetime of the activity.
+ *
+ * @return Task identifier, an opaque integer.
+ */
+ public int getTaskId() {
+ try {
+ return ActivityManager.getService()
+ .getTaskForActivity(mToken, false);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Return whether this activity is the root of a task. The root is the
+ * first activity in a task.
+ *
+ * @return True if this is the root activity, else false.
+ */
+ @Override
+ public boolean isTaskRoot() {
+ try {
+ return ActivityManager.getService().getTaskForActivity(mToken, true) >= 0;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Move the task containing this activity to the back of the activity
+ * stack. The activity's order within the task is unchanged.
+ *
+ * @param nonRoot If false then this only works if the activity is the root
+ * of a task; if true it will work for any activity in
+ * a task.
+ *
+ * @return If the task was moved (or it was already at the
+ * back) true is returned, else false.
+ */
+ public boolean moveTaskToBack(boolean nonRoot) {
+ try {
+ return ActivityManager.getService().moveActivityTaskToBack(
+ mToken, nonRoot);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ /**
+ * Returns class name for this activity with the package prefix removed.
+ * This is the default name used to read and write settings.
+ *
+ * @return The local class name.
+ */
+ @NonNull
+ public String getLocalClassName() {
+ final String pkg = getPackageName();
+ final String cls = mComponent.getClassName();
+ int packageLen = pkg.length();
+ if (!cls.startsWith(pkg) || cls.length() <= packageLen
+ || cls.charAt(packageLen) != '.') {
+ return cls;
+ }
+ return cls.substring(packageLen+1);
+ }
+
+ /**
+ * Returns complete component name of this activity.
+ *
+ * @return Returns the complete component name for this activity
+ */
+ public ComponentName getComponentName()
+ {
+ return mComponent;
+ }
+
+ /**
+ * Retrieve a {@link SharedPreferences} object for accessing preferences
+ * that are private to this activity. This simply calls the underlying
+ * {@link #getSharedPreferences(String, int)} method by passing in this activity's
+ * class name as the preferences name.
+ *
+ * @param mode Operating mode. Use {@link #MODE_PRIVATE} for the default
+ * operation.
+ *
+ * @return Returns the single SharedPreferences instance that can be used
+ * to retrieve and modify the preference values.
+ */
+ public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
+ return getSharedPreferences(getLocalClassName(), mode);
+ }
+
+ private void ensureSearchManager() {
+ if (mSearchManager != null) {
+ return;
+ }
+
+ try {
+ mSearchManager = new SearchManager(this, null);
+ } catch (ServiceNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public Object getSystemService(@ServiceName @NonNull String name) {
+ if (getBaseContext() == null) {
+ throw new IllegalStateException(
+ "System services not available to Activities before onCreate()");
+ }
+
+ if (WINDOW_SERVICE.equals(name)) {
+ return mWindowManager;
+ } else if (SEARCH_SERVICE.equals(name)) {
+ ensureSearchManager();
+ return mSearchManager;
+ }
+ return super.getSystemService(name);
+ }
+
+ /**
+ * Change the title associated with this activity. If this is a
+ * top-level activity, the title for its window will change. If it
+ * is an embedded activity, the parent can do whatever it wants
+ * with it.
+ */
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ onTitleChanged(title, mTitleColor);
+
+ if (mParent != null) {
+ mParent.onChildTitleChanged(this, title);
+ }
+ }
+
+ /**
+ * Change the title associated with this activity. If this is a
+ * top-level activity, the title for its window will change. If it
+ * is an embedded activity, the parent can do whatever it wants
+ * with it.
+ */
+ public void setTitle(int titleId) {
+ setTitle(getText(titleId));
+ }
+
+ /**
+ * Change the color of the title associated with this activity.
+ * <p>
+ * This method is deprecated starting in API Level 11 and replaced by action
+ * bar styles. For information on styling the Action Bar, read the <a
+ * href="{@docRoot} guide/topics/ui/actionbar.html">Action Bar</a> developer
+ * guide.
+ *
+ * @deprecated Use action bar styles instead.
+ */
+ @Deprecated
+ public void setTitleColor(int textColor) {
+ mTitleColor = textColor;
+ onTitleChanged(mTitle, textColor);
+ }
+
+ public final CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public final int getTitleColor() {
+ return mTitleColor;
+ }
+
+ protected void onTitleChanged(CharSequence title, int color) {
+ if (mTitleReady) {
+ final Window win = getWindow();
+ if (win != null) {
+ win.setTitle(title);
+ if (color != 0) {
+ win.setTitleColor(color);
+ }
+ }
+ if (mActionBar != null) {
+ mActionBar.setWindowTitle(title);
+ }
+ }
+ }
+
+ protected void onChildTitleChanged(Activity childActivity, CharSequence title) {
+ }
+
+ /**
+ * Sets information describing the task with this activity for presentation inside the Recents
+ * System UI. When {@link ActivityManager#getRecentTasks} is called, the activities of each task
+ * are traversed in order from the topmost activity to the bottommost. The traversal continues
+ * for each property until a suitable value is found. For each task the taskDescription will be
+ * returned in {@link android.app.ActivityManager.TaskDescription}.
+ *
+ * @see ActivityManager#getRecentTasks
+ * @see android.app.ActivityManager.TaskDescription
+ *
+ * @param taskDescription The TaskDescription properties that describe the task with this activity
+ */
+ public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
+ if (mTaskDescription != taskDescription) {
+ mTaskDescription.copyFromPreserveHiddenFields(taskDescription);
+ // Scale the icon down to something reasonable if it is provided
+ if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
+ final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
+ final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
+ true);
+ mTaskDescription.setIcon(icon);
+ }
+ }
+ try {
+ ActivityManager.getService().setTaskDescription(mToken, mTaskDescription);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Sets the visibility of the progress bar in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param visible Whether to show the progress bars in the title.
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public final void setProgressBarVisibility(boolean visible) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS, visible ? Window.PROGRESS_VISIBILITY_ON :
+ Window.PROGRESS_VISIBILITY_OFF);
+ }
+
+ /**
+ * Sets the visibility of the indeterminate progress bar in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param visible Whether to show the progress bars in the title.
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public final void setProgressBarIndeterminateVisibility(boolean visible) {
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ visible ? Window.PROGRESS_VISIBILITY_ON : Window.PROGRESS_VISIBILITY_OFF);
+ }
+
+ /**
+ * Sets whether the horizontal progress bar in the title should be indeterminate (the circular
+ * is always indeterminate).
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param indeterminate Whether the horizontal progress bar should be indeterminate.
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public final void setProgressBarIndeterminate(boolean indeterminate) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+ indeterminate ? Window.PROGRESS_INDETERMINATE_ON
+ : Window.PROGRESS_INDETERMINATE_OFF);
+ }
+
+ /**
+ * Sets the progress for the progress bars in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param progress The progress for the progress bar. Valid ranges are from
+ * 0 to 10000 (both inclusive). If 10000 is given, the progress
+ * bar will be completely filled and will fade out.
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public final void setProgress(int progress) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS, progress + Window.PROGRESS_START);
+ }
+
+ /**
+ * Sets the secondary progress for the progress bar in the title. This
+ * progress is drawn between the primary progress (set via
+ * {@link #setProgress(int)} and the background. It can be ideal for media
+ * scenarios such as showing the buffering progress while the default
+ * progress shows the play progress.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param secondaryProgress The secondary progress for the progress bar. Valid ranges are from
+ * 0 to 10000 (both inclusive).
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public final void setSecondaryProgress(int secondaryProgress) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+ secondaryProgress + Window.PROGRESS_SECONDARY_START);
+ }
+
+ /**
+ * Suggests an audio stream whose volume should be changed by the hardware
+ * volume controls.
+ * <p>
+ * The suggested audio stream will be tied to the window of this Activity.
+ * Volume requests which are received while the Activity is in the
+ * foreground will affect this stream.
+ * <p>
+ * It is not guaranteed that the hardware volume controls will always change
+ * this stream's volume (for example, if a call is in progress, its stream's
+ * volume may be changed instead). To reset back to the default, use
+ * {@link AudioManager#USE_DEFAULT_STREAM_TYPE}.
+ *
+ * @param streamType The type of the audio stream whose volume should be
+ * changed by the hardware volume controls.
+ */
+ public final void setVolumeControlStream(int streamType) {
+ getWindow().setVolumeControlStream(streamType);
+ }
+
+ /**
+ * Gets the suggested audio stream whose volume should be changed by the
+ * hardware volume controls.
+ *
+ * @return The suggested audio stream type whose volume should be changed by
+ * the hardware volume controls.
+ * @see #setVolumeControlStream(int)
+ */
+ public final int getVolumeControlStream() {
+ return getWindow().getVolumeControlStream();
+ }
+
+ /**
+ * Sets a {@link MediaController} to send media keys and volume changes to.
+ * <p>
+ * The controller will be tied to the window of this Activity. Media key and
+ * volume events which are received while the Activity is in the foreground
+ * will be forwarded to the controller and used to invoke transport controls
+ * or adjust the volume. This may be used instead of or in addition to
+ * {@link #setVolumeControlStream} to affect a specific session instead of a
+ * specific stream.
+ * <p>
+ * It is not guaranteed that the hardware volume controls will always change
+ * this session's volume (for example, if a call is in progress, its
+ * stream's volume may be changed instead). To reset back to the default use
+ * null as the controller.
+ *
+ * @param controller The controller for the session which should receive
+ * media keys and volume changes.
+ */
+ public final void setMediaController(MediaController controller) {
+ getWindow().setMediaController(controller);
+ }
+
+ /**
+ * Gets the controller which should be receiving media key and volume events
+ * while this activity is in the foreground.
+ *
+ * @return The controller which should receive events.
+ * @see #setMediaController(android.media.session.MediaController)
+ */
+ public final MediaController getMediaController() {
+ return getWindow().getMediaController();
+ }
+
+ /**
+ * Runs the specified action on the UI thread. If the current thread is the UI
+ * thread, then the action is executed immediately. If the current thread is
+ * not the UI thread, the action is posted to the event queue of the UI thread.
+ *
+ * @param action the action to run on the UI thread
+ */
+ @Override
+ public final void runOnUiThread(Runnable action) {
+ if (Thread.currentThread() != mUiThread) {
+ mHandler.post(action);
+ } else {
+ action.run();
+ }
+ }
+
+ /**
+ * Standard implementation of
+ * {@link android.view.LayoutInflater.Factory#onCreateView} used when
+ * inflating with the LayoutInflater returned by {@link #getSystemService}.
+ * This implementation does nothing and is for
+ * pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps. Newer apps
+ * should use {@link #onCreateView(View, String, Context, AttributeSet)}.
+ *
+ * @see android.view.LayoutInflater#createView
+ * @see android.view.Window#getLayoutInflater
+ */
+ @Nullable
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return null;
+ }
+
+ /**
+ * Standard implementation of
+ * {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)}
+ * used when inflating with the LayoutInflater returned by {@link #getSystemService}.
+ * This implementation handles <fragment> tags to embed fragments inside
+ * of the activity.
+ *
+ * @see android.view.LayoutInflater#createView
+ * @see android.view.Window#getLayoutInflater
+ */
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ if (!"fragment".equals(name)) {
+ return onCreateView(name, context, attrs);
+ }
+
+ return mFragments.onCreateView(parent, name, context, attrs);
+ }
+
+ /**
+ * Print the Activity's state into the given stream. This gets invoked if
+ * you run "adb shell dumpsys activity &lt;activity_component_name&gt;".
+ *
+ * @param prefix Desired prefix to prepend at each line of output.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ dumpInner(prefix, fd, writer, args);
+ }
+
+ void dumpInner(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("Local Activity ");
+ writer.print(Integer.toHexString(System.identityHashCode(this)));
+ writer.println(" State:");
+ String innerPrefix = prefix + " ";
+ writer.print(innerPrefix); writer.print("mResumed=");
+ writer.print(mResumed); writer.print(" mStopped=");
+ writer.print(mStopped); writer.print(" mFinished=");
+ writer.println(mFinished);
+ writer.print(innerPrefix); writer.print("mChangingConfigurations=");
+ writer.println(mChangingConfigurations);
+ writer.print(innerPrefix); writer.print("mCurrentConfig=");
+ writer.println(mCurrentConfig);
+
+ mFragments.dumpLoaders(innerPrefix, fd, writer, args);
+ mFragments.getFragmentManager().dump(innerPrefix, fd, writer, args);
+ if (mVoiceInteractor != null) {
+ mVoiceInteractor.dump(innerPrefix, fd, writer, args);
+ }
+
+ if (getWindow() != null &&
+ getWindow().peekDecorView() != null &&
+ getWindow().peekDecorView().getViewRootImpl() != null) {
+ getWindow().peekDecorView().getViewRootImpl().dump(prefix, fd, writer, args);
+ }
+
+ mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
+
+ final AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ afm.dump(prefix, writer);
+ }
+ }
+
+ /**
+ * Bit indicating that this activity is "immersive" and should not be
+ * interrupted by notifications if possible.
+ *
+ * This value is initially set by the manifest property
+ * <code>android:immersive</code> but may be changed at runtime by
+ * {@link #setImmersive}.
+ *
+ * @see #setImmersive(boolean)
+ * @see android.content.pm.ActivityInfo#FLAG_IMMERSIVE
+ */
+ public boolean isImmersive() {
+ try {
+ return ActivityManager.getService().isImmersive(mToken);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Indication of whether this is the highest level activity in this task. Can be used to
+ * determine whether an activity launched by this activity was placed in the same task or
+ * another task.
+ *
+ * @return true if this is the topmost, non-finishing activity in its task.
+ */
+ private boolean isTopOfTask() {
+ if (mToken == null || mWindow == null) {
+ return false;
+ }
+ try {
+ return ActivityManager.getService().isTopOfTask(getActivityToken());
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} to a
+ * fullscreen opaque Activity.
+ * <p>
+ * Call this whenever the background of a translucent Activity has changed to become opaque.
+ * Doing so will allow the {@link android.view.Surface} of the Activity behind to be released.
+ * <p>
+ * This call has no effect on non-translucent activities or on activities with the
+ * {@link android.R.attr#windowIsFloating} attribute.
+ *
+ * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener,
+ * ActivityOptions)
+ * @see TranslucentConversionListener
+ *
+ * @hide
+ */
+ @SystemApi
+ public void convertFromTranslucent() {
+ try {
+ mTranslucentCallback = null;
+ if (ActivityManager.getService().convertFromTranslucent(mToken)) {
+ WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, true);
+ }
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from
+ * opaque to translucent following a call to {@link #convertFromTranslucent()}.
+ * <p>
+ * Calling this allows the Activity behind this one to be seen again. Once all such Activities
+ * have been redrawn {@link TranslucentConversionListener#onTranslucentConversionComplete} will
+ * be called indicating that it is safe to make this activity translucent again. Until
+ * {@link TranslucentConversionListener#onTranslucentConversionComplete} is called the image
+ * behind the frontmost Activity will be indeterminate.
+ * <p>
+ * This call has no effect on non-translucent activities or on activities with the
+ * {@link android.R.attr#windowIsFloating} attribute.
+ *
+ * @param callback the method to call when all visible Activities behind this one have been
+ * drawn and it is safe to make this Activity translucent again.
+ * @param options activity options delivered to the activity below this one. The options
+ * are retrieved using {@link #getActivityOptions}.
+ * @return <code>true</code> if Window was opaque and will become translucent or
+ * <code>false</code> if window was translucent and no change needed to be made.
+ *
+ * @see #convertFromTranslucent()
+ * @see TranslucentConversionListener
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean convertToTranslucent(TranslucentConversionListener callback,
+ ActivityOptions options) {
+ boolean drawComplete;
+ try {
+ mTranslucentCallback = callback;
+ mChangeCanvasToTranslucent = ActivityManager.getService().convertToTranslucent(
+ mToken, options == null ? null : options.toBundle());
+ WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, false);
+ drawComplete = true;
+ } catch (RemoteException e) {
+ // Make callback return as though it timed out.
+ mChangeCanvasToTranslucent = false;
+ drawComplete = false;
+ }
+ if (!mChangeCanvasToTranslucent && mTranslucentCallback != null) {
+ // Window is already translucent.
+ mTranslucentCallback.onTranslucentConversionComplete(drawComplete);
+ }
+ return mChangeCanvasToTranslucent;
+ }
+
+ /** @hide */
+ void onTranslucentConversionComplete(boolean drawComplete) {
+ if (mTranslucentCallback != null) {
+ mTranslucentCallback.onTranslucentConversionComplete(drawComplete);
+ mTranslucentCallback = null;
+ }
+ if (mChangeCanvasToTranslucent) {
+ WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, false);
+ }
+ }
+
+ /** @hide */
+ public void onNewActivityOptions(ActivityOptions options) {
+ mActivityTransitionState.setEnterActivityOptions(this, options);
+ if (!mStopped) {
+ mActivityTransitionState.enterReady(this);
+ }
+ }
+
+ /**
+ * Retrieve the ActivityOptions passed in from the launching activity or passed back
+ * from an activity launched by this activity in its call to {@link
+ * #convertToTranslucent(TranslucentConversionListener, ActivityOptions)}
+ *
+ * @return The ActivityOptions passed to {@link #convertToTranslucent}.
+ * @hide
+ */
+ ActivityOptions getActivityOptions() {
+ try {
+ return ActivityOptions.fromBundle(
+ ActivityManager.getService().getActivityOptions(mToken));
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Activities that want to remain visible behind a translucent activity above them must call
+ * this method anytime between the start of {@link #onResume()} and the return from
+ * {@link #onPause()}. If this call is successful then the activity will remain visible after
+ * {@link #onPause()} is called, and is allowed to continue playing media in the background.
+ *
+ * <p>The actions of this call are reset each time that this activity is brought to the
+ * front. That is, every time {@link #onResume()} is called the activity will be assumed
+ * to not have requested visible behind. Therefore, if you want this activity to continue to
+ * be visible in the background you must call this method again.
+ *
+ * <p>Only fullscreen opaque activities may make this call. I.e. this call is a nop
+ * for dialog and translucent activities.
+ *
+ * <p>Under all circumstances, the activity must stop playing and release resources prior to or
+ * within a call to {@link #onVisibleBehindCanceled()} or if this call returns false.
+ *
+ * <p>False will be returned any time this method is called between the return of onPause and
+ * the next call to onResume.
+ *
+ * @deprecated This method's functionality is no longer supported as of
+ * {@link android.os.Build.VERSION_CODES#O} and will be removed in a future release.
+ *
+ * @param visible true to notify the system that the activity wishes to be visible behind other
+ * translucent activities, false to indicate otherwise. Resources must be
+ * released when passing false to this method.
+ *
+ * @return the resulting visibiity state. If true the activity will remain visible beyond
+ * {@link #onPause()} if the next activity is translucent or not fullscreen. If false
+ * then the activity may not count on being visible behind other translucent activities,
+ * and must stop any media playback and release resources.
+ * Returning false may occur in lieu of a call to {@link #onVisibleBehindCanceled()} so
+ * the return value must be checked.
+ *
+ * @see #onVisibleBehindCanceled()
+ */
+ @Deprecated
+ public boolean requestVisibleBehind(boolean visible) {
+ return false;
+ }
+
+ /**
+ * Called when a translucent activity over this activity is becoming opaque or another
+ * activity is being launched. Activities that override this method must call
+ * <code>super.onVisibleBehindCanceled()</code> or a SuperNotCalledException will be thrown.
+ *
+ * <p>When this method is called the activity has 500 msec to release any resources it may be
+ * using while visible in the background.
+ * If the activity has not returned from this method in 500 msec the system will destroy
+ * the activity and kill the process in order to recover the resources for another
+ * process. Otherwise {@link #onStop()} will be called following return.
+ *
+ * @see #requestVisibleBehind(boolean)
+ *
+ * @deprecated This method's functionality is no longer supported as of
+ * {@link android.os.Build.VERSION_CODES#O} and will be removed in a future release.
+ */
+ @Deprecated
+ @CallSuper
+ public void onVisibleBehindCanceled() {
+ mCalled = true;
+ }
+
+ /**
+ * Translucent activities may call this to determine if there is an activity below them that
+ * is currently set to be visible in the background.
+ *
+ * @deprecated This method's functionality is no longer supported as of
+ * {@link android.os.Build.VERSION_CODES#O} and will be removed in a future release.
+ *
+ * @return true if an activity below is set to visible according to the most recent call to
+ * {@link #requestVisibleBehind(boolean)}, false otherwise.
+ *
+ * @see #requestVisibleBehind(boolean)
+ * @see #onVisibleBehindCanceled()
+ * @see #onBackgroundVisibleBehindChanged(boolean)
+ * @hide
+ */
+ @Deprecated
+ @SystemApi
+ public boolean isBackgroundVisibleBehind() {
+ return false;
+ }
+
+ /**
+ * The topmost foreground activity will receive this call when the background visibility state
+ * of the activity below it changes.
+ *
+ * This call may be a consequence of {@link #requestVisibleBehind(boolean)} or might be
+ * due to a background activity finishing itself.
+ *
+ * @deprecated This method's functionality is no longer supported as of
+ * {@link android.os.Build.VERSION_CODES#O} and will be removed in a future release.
+ *
+ * @param visible true if a background activity is visible, false otherwise.
+ *
+ * @see #requestVisibleBehind(boolean)
+ * @see #onVisibleBehindCanceled()
+ * @hide
+ */
+ @Deprecated
+ @SystemApi
+ public void onBackgroundVisibleBehindChanged(boolean visible) {
+ }
+
+ /**
+ * Activities cannot draw during the period that their windows are animating in. In order
+ * to know when it is safe to begin drawing they can override this method which will be
+ * called when the entering animation has completed.
+ */
+ public void onEnterAnimationComplete() {
+ }
+
+ /**
+ * @hide
+ */
+ public void dispatchEnterAnimationComplete() {
+ onEnterAnimationComplete();
+ if (getWindow() != null && getWindow().getDecorView() != null) {
+ getWindow().getDecorView().getViewTreeObserver().dispatchOnEnterAnimationComplete();
+ }
+ }
+
+ /**
+ * Adjust the current immersive mode setting.
+ *
+ * Note that changing this value will have no effect on the activity's
+ * {@link android.content.pm.ActivityInfo} structure; that is, if
+ * <code>android:immersive</code> is set to <code>true</code>
+ * in the application's manifest entry for this activity, the {@link
+ * android.content.pm.ActivityInfo#flags ActivityInfo.flags} member will
+ * always have its {@link android.content.pm.ActivityInfo#FLAG_IMMERSIVE
+ * FLAG_IMMERSIVE} bit set.
+ *
+ * @see #isImmersive()
+ * @see android.content.pm.ActivityInfo#FLAG_IMMERSIVE
+ */
+ public void setImmersive(boolean i) {
+ try {
+ ActivityManager.getService().setImmersive(mToken, i);
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Enable or disable virtual reality (VR) mode for this Activity.
+ *
+ * <p>VR mode is a hint to Android system to switch to a mode optimized for VR applications
+ * while this Activity has user focus.</p>
+ *
+ * <p>It is recommended that applications additionally declare
+ * {@link android.R.attr#enableVrMode} in their manifest to allow for smooth activity
+ * transitions when switching between VR activities.</p>
+ *
+ * <p>If the requested {@link android.service.vr.VrListenerService} component is not available,
+ * VR mode will not be started. Developers can handle this case as follows:</p>
+ *
+ * <pre>
+ * String servicePackage = "com.whatever.app";
+ * String serviceClass = "com.whatever.app.MyVrListenerService";
+ *
+ * // Name of the component of the VrListenerService to start.
+ * ComponentName serviceComponent = new ComponentName(servicePackage, serviceClass);
+ *
+ * try {
+ * setVrModeEnabled(true, myComponentName);
+ * } catch (PackageManager.NameNotFoundException e) {
+ * List&lt;ApplicationInfo> installed = getPackageManager().getInstalledApplications(0);
+ * boolean isInstalled = false;
+ * for (ApplicationInfo app : installed) {
+ * if (app.packageName.equals(servicePackage)) {
+ * isInstalled = true;
+ * break;
+ * }
+ * }
+ * if (isInstalled) {
+ * // Package is installed, but not enabled in Settings. Let user enable it.
+ * startActivity(new Intent(Settings.ACTION_VR_LISTENER_SETTINGS));
+ * } else {
+ * // Package is not installed. Send an intent to download this.
+ * sentIntentToLaunchAppStore(servicePackage);
+ * }
+ * }
+ * </pre>
+ *
+ * @param enabled {@code true} to enable this mode.
+ * @param requestedComponent the name of the component to use as a
+ * {@link android.service.vr.VrListenerService} while VR mode is enabled.
+ *
+ * @throws android.content.pm.PackageManager.NameNotFoundException if the given component
+ * to run as a {@link android.service.vr.VrListenerService} is not installed, or has
+ * not been enabled in user settings.
+ *
+ * @see android.content.pm.PackageManager#FEATURE_VR_MODE
+ * @see android.content.pm.PackageManager#FEATURE_VR_MODE_HIGH_PERFORMANCE
+ * @see android.service.vr.VrListenerService
+ * @see android.provider.Settings#ACTION_VR_LISTENER_SETTINGS
+ * @see android.R.attr#enableVrMode
+ */
+ public void setVrModeEnabled(boolean enabled, @NonNull ComponentName requestedComponent)
+ throws PackageManager.NameNotFoundException {
+ try {
+ if (ActivityManager.getService().setVrMode(mToken, enabled, requestedComponent)
+ != 0) {
+ throw new PackageManager.NameNotFoundException(
+ requestedComponent.flattenToString());
+ }
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Start an action mode of the default type {@link ActionMode#TYPE_PRIMARY}.
+ *
+ * @param callback Callback that will manage lifecycle events for this action mode
+ * @return The ActionMode that was started, or null if it was canceled
+ *
+ * @see ActionMode
+ */
+ @Nullable
+ public ActionMode startActionMode(ActionMode.Callback callback) {
+ return mWindow.getDecorView().startActionMode(callback);
+ }
+
+ /**
+ * Start an action mode of the given type.
+ *
+ * @param callback Callback that will manage lifecycle events for this action mode
+ * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}.
+ * @return The ActionMode that was started, or null if it was canceled
+ *
+ * @see ActionMode
+ */
+ @Nullable
+ public ActionMode startActionMode(ActionMode.Callback callback, int type) {
+ return mWindow.getDecorView().startActionMode(callback, type);
+ }
+
+ /**
+ * Give the Activity a chance to control the UI for an action mode requested
+ * by the system.
+ *
+ * <p>Note: If you are looking for a notification callback that an action mode
+ * has been started for this activity, see {@link #onActionModeStarted(ActionMode)}.</p>
+ *
+ * @param callback The callback that should control the new action mode
+ * @return The new action mode, or <code>null</code> if the activity does not want to
+ * provide special handling for this action mode. (It will be handled by the system.)
+ */
+ @Nullable
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
+ // Only Primary ActionModes are represented in the ActionBar.
+ if (mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) {
+ initWindowDecorActionBar();
+ if (mActionBar != null) {
+ return mActionBar.startActionMode(callback);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Nullable
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) {
+ try {
+ mActionModeTypeStarting = type;
+ return onWindowStartingActionMode(callback);
+ } finally {
+ mActionModeTypeStarting = ActionMode.TYPE_PRIMARY;
+ }
+ }
+
+ /**
+ * Notifies the Activity that an action mode has been started.
+ * Activity subclasses overriding this method should call the superclass implementation.
+ *
+ * @param mode The new action mode.
+ */
+ @CallSuper
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ }
+
+ /**
+ * Notifies the activity that an action mode has finished.
+ * Activity subclasses overriding this method should call the superclass implementation.
+ *
+ * @param mode The action mode that just finished.
+ */
+ @CallSuper
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ }
+
+ /**
+ * Returns true if the app should recreate the task when navigating 'up' from this activity
+ * by using targetIntent.
+ *
+ * <p>If this method returns false the app can trivially call
+ * {@link #navigateUpTo(Intent)} using the same parameters to correctly perform
+ * up navigation. If this method returns false, the app should synthesize a new task stack
+ * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.</p>
+ *
+ * @param targetIntent An intent representing the target destination for up navigation
+ * @return true if navigating up should recreate a new task stack, false if the same task
+ * should be used for the destination
+ */
+ public boolean shouldUpRecreateTask(Intent targetIntent) {
+ try {
+ PackageManager pm = getPackageManager();
+ ComponentName cn = targetIntent.getComponent();
+ if (cn == null) {
+ cn = targetIntent.resolveActivity(pm);
+ }
+ ActivityInfo info = pm.getActivityInfo(cn, 0);
+ if (info.taskAffinity == null) {
+ return false;
+ }
+ return ActivityManager.getService()
+ .shouldUpRecreateTask(mToken, info.taskAffinity);
+ } catch (RemoteException e) {
+ return false;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Navigate from this activity to the activity specified by upIntent, finishing this activity
+ * in the process. If the activity indicated by upIntent already exists in the task's history,
+ * this activity and all others before the indicated activity in the history stack will be
+ * finished.
+ *
+ * <p>If the indicated activity does not appear in the history stack, this will finish
+ * each activity in this task until the root activity of the task is reached, resulting in
+ * an "in-app home" behavior. This can be useful in apps with a complex navigation hierarchy
+ * when an activity may be reached by a path not passing through a canonical parent
+ * activity.</p>
+ *
+ * <p>This method should be used when performing up navigation from within the same task
+ * as the destination. If up navigation should cross tasks in some cases, see
+ * {@link #shouldUpRecreateTask(Intent)}.</p>
+ *
+ * @param upIntent An intent representing the target destination for up navigation
+ *
+ * @return true if up navigation successfully reached the activity indicated by upIntent and
+ * upIntent was delivered to it. false if an instance of the indicated activity could
+ * not be found and this activity was simply finished normally.
+ */
+ public boolean navigateUpTo(Intent upIntent) {
+ if (mParent == null) {
+ ComponentName destInfo = upIntent.getComponent();
+ if (destInfo == null) {
+ destInfo = upIntent.resolveActivity(getPackageManager());
+ if (destInfo == null) {
+ return false;
+ }
+ upIntent = new Intent(upIntent);
+ upIntent.setComponent(destInfo);
+ }
+ int resultCode;
+ Intent resultData;
+ synchronized (this) {
+ resultCode = mResultCode;
+ resultData = mResultData;
+ }
+ if (resultData != null) {
+ resultData.prepareToLeaveProcess(this);
+ }
+ try {
+ upIntent.prepareToLeaveProcess(this);
+ return ActivityManager.getService().navigateUpTo(mToken, upIntent,
+ resultCode, resultData);
+ } catch (RemoteException e) {
+ return false;
+ }
+ } else {
+ return mParent.navigateUpToFromChild(this, upIntent);
+ }
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * {@link #navigateUpTo} method. The default implementation simply calls
+ * navigateUpTo(upIntent) on this activity (the parent).
+ *
+ * @param child The activity making the call.
+ * @param upIntent An intent representing the target destination for up navigation
+ *
+ * @return true if up navigation successfully reached the activity indicated by upIntent and
+ * upIntent was delivered to it. false if an instance of the indicated activity could
+ * not be found and this activity was simply finished normally.
+ */
+ public boolean navigateUpToFromChild(Activity child, Intent upIntent) {
+ return navigateUpTo(upIntent);
+ }
+
+ /**
+ * Obtain an {@link Intent} that will launch an explicit target activity specified by
+ * this activity's logical parent. The logical parent is named in the application's manifest
+ * by the {@link android.R.attr#parentActivityName parentActivityName} attribute.
+ * Activity subclasses may override this method to modify the Intent returned by
+ * super.getParentActivityIntent() or to implement a different mechanism of retrieving
+ * the parent intent entirely.
+ *
+ * @return a new Intent targeting the defined parent of this activity or null if
+ * there is no valid parent.
+ */
+ @Nullable
+ public Intent getParentActivityIntent() {
+ final String parentName = mActivityInfo.parentActivityName;
+ if (TextUtils.isEmpty(parentName)) {
+ return null;
+ }
+
+ // If the parent itself has no parent, generate a main activity intent.
+ final ComponentName target = new ComponentName(this, parentName);
+ try {
+ final ActivityInfo parentInfo = getPackageManager().getActivityInfo(target, 0);
+ final String parentActivity = parentInfo.parentActivityName;
+ final Intent parentIntent = parentActivity == null
+ ? Intent.makeMainActivity(target)
+ : new Intent().setComponent(target);
+ return parentIntent;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "getParentActivityIntent: bad parentActivityName '" + parentName +
+ "' in manifest");
+ return null;
+ }
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>callback</var>
+ * will be called to handle shared elements on the <i>launched</i> Activity. This requires
+ * {@link Window#FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param callback Used to manipulate shared element transitions on the launched Activity.
+ */
+ public void setEnterSharedElementCallback(SharedElementCallback callback) {
+ if (callback == null) {
+ callback = SharedElementCallback.NULL_CALLBACK;
+ }
+ mEnterTransitionListener = callback;
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>callback</var>
+ * will be called to handle shared elements on the <i>launching</i> Activity. Most
+ * calls will only come when returning from the started Activity.
+ * This requires {@link Window#FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param callback Used to manipulate shared element transitions on the launching Activity.
+ */
+ public void setExitSharedElementCallback(SharedElementCallback callback) {
+ if (callback == null) {
+ callback = SharedElementCallback.NULL_CALLBACK;
+ }
+ mExitTransitionListener = callback;
+ }
+
+ /**
+ * Postpone the entering activity transition when Activity was started with
+ * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.util.Pair[])}.
+ * <p>This method gives the Activity the ability to delay starting the entering and
+ * shared element transitions until all data is loaded. Until then, the Activity won't
+ * draw into its window, leaving the window transparent. This may also cause the
+ * returning animation to be delayed until data is ready. This method should be
+ * called in {@link #onCreate(android.os.Bundle)} or in
+ * {@link #onActivityReenter(int, android.content.Intent)}.
+ * {@link #startPostponedEnterTransition()} must be called to allow the Activity to
+ * start the transitions. If the Activity did not use
+ * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.util.Pair[])}, then this method does nothing.</p>
+ */
+ public void postponeEnterTransition() {
+ mActivityTransitionState.postponeEnterTransition();
+ }
+
+ /**
+ * Begin postponed transitions after {@link #postponeEnterTransition()} was called.
+ * If postponeEnterTransition() was called, you must call startPostponedEnterTransition()
+ * to have your Activity start drawing.
+ */
+ public void startPostponedEnterTransition() {
+ mActivityTransitionState.startPostponedEnterTransition();
+ }
+
+ /**
+ * Create {@link DragAndDropPermissions} object bound to this activity and controlling the
+ * access permissions for content URIs associated with the {@link DragEvent}.
+ * @param event Drag event
+ * @return The {@link DragAndDropPermissions} object used to control access to the content URIs.
+ * Null if no content URIs are associated with the event or if permissions could not be granted.
+ */
+ public DragAndDropPermissions requestDragAndDropPermissions(DragEvent event) {
+ DragAndDropPermissions dragAndDropPermissions = DragAndDropPermissions.obtain(event);
+ if (dragAndDropPermissions != null && dragAndDropPermissions.take(getActivityToken())) {
+ return dragAndDropPermissions;
+ }
+ return null;
+ }
+
+ // ------------------ Internal API ------------------
+
+ final void setParent(Activity parent) {
+ mParent = parent;
+ }
+
+ final void attach(Context context, ActivityThread aThread,
+ Instrumentation instr, IBinder token, int ident,
+ Application application, Intent intent, ActivityInfo info,
+ CharSequence title, Activity parent, String id,
+ NonConfigurationInstances lastNonConfigurationInstances,
+ Configuration config, String referrer, IVoiceInteractor voiceInteractor,
+ Window window, ActivityConfigCallback activityConfigCallback) {
+ attachBaseContext(context);
+
+ mFragments.attachHost(null /*parent*/);
+
+ mWindow = new PhoneWindow(this, window, activityConfigCallback);
+ mWindow.setWindowControllerCallback(this);
+ mWindow.setCallback(this);
+ mWindow.setOnWindowDismissedCallback(this);
+ mWindow.getLayoutInflater().setPrivateFactory(this);
+ if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
+ mWindow.setSoftInputMode(info.softInputMode);
+ }
+ if (info.uiOptions != 0) {
+ mWindow.setUiOptions(info.uiOptions);
+ }
+ mUiThread = Thread.currentThread();
+
+ mMainThread = aThread;
+ mInstrumentation = instr;
+ mToken = token;
+ mIdent = ident;
+ mApplication = application;
+ mIntent = intent;
+ mReferrer = referrer;
+ mComponent = intent.getComponent();
+ mActivityInfo = info;
+ mTitle = title;
+ mParent = parent;
+ mEmbeddedID = id;
+ mLastNonConfigurationInstances = lastNonConfigurationInstances;
+ if (voiceInteractor != null) {
+ if (lastNonConfigurationInstances != null) {
+ mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
+ } else {
+ mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
+ Looper.myLooper());
+ }
+ }
+
+ mWindow.setWindowManager(
+ (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
+ mToken, mComponent.flattenToString(),
+ (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
+ if (mParent != null) {
+ mWindow.setContainer(mParent.getWindow());
+ }
+ mWindowManager = mWindow.getWindowManager();
+ mCurrentConfig = config;
+
+ mWindow.setColorMode(info.colorMode);
+ }
+
+ /** @hide */
+ public final IBinder getActivityToken() {
+ return mParent != null ? mParent.getActivityToken() : mToken;
+ }
+
+ final void performCreate(Bundle icicle) {
+ performCreate(icicle, null);
+ }
+
+ final void performCreate(Bundle icicle, PersistableBundle persistentState) {
+ mCanEnterPictureInPicture = true;
+ restoreHasCurrentPermissionRequest(icicle);
+ if (persistentState != null) {
+ onCreate(icicle, persistentState);
+ } else {
+ onCreate(icicle);
+ }
+ mActivityTransitionState.readState(icicle);
+
+ mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
+ com.android.internal.R.styleable.Window_windowNoDisplay, false);
+ mFragments.dispatchActivityCreated();
+ mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
+ }
+
+ final void performNewIntent(Intent intent) {
+ mCanEnterPictureInPicture = true;
+ onNewIntent(intent);
+ }
+
+ final void performStart() {
+ mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
+ mFragments.noteStateNotSaved();
+ mCalled = false;
+ mFragments.execPendingActions();
+ mInstrumentation.callActivityOnStart(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onStart()");
+ }
+ mFragments.dispatchStart();
+ mFragments.reportLoaderStart();
+
+ // This property is set for all builds except final release
+ boolean isDlwarningEnabled = SystemProperties.getInt("ro.bionic.ld.warning", 0) == 1;
+ boolean isAppDebuggable =
+ (mApplication.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+
+ if (isAppDebuggable || isDlwarningEnabled) {
+ String dlwarning = getDlWarning();
+ if (dlwarning != null) {
+ String appName = getApplicationInfo().loadLabel(getPackageManager())
+ .toString();
+ String warning = "Detected problems with app native libraries\n" +
+ "(please consult log for detail):\n" + dlwarning;
+ if (isAppDebuggable) {
+ new AlertDialog.Builder(this).
+ setTitle(appName).
+ setMessage(warning).
+ setPositiveButton(android.R.string.ok, null).
+ setCancelable(false).
+ show();
+ } else {
+ Toast.makeText(this, appName + "\n" + warning, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ mActivityTransitionState.enterReady(this);
+ }
+
+ final void performRestart() {
+ mCanEnterPictureInPicture = true;
+ mFragments.noteStateNotSaved();
+
+ if (mToken != null && mParent == null) {
+ // No need to check mStopped, the roots will check if they were actually stopped.
+ WindowManagerGlobal.getInstance().setStoppedState(mToken, false /* stopped */);
+ }
+
+ if (mStopped) {
+ mStopped = false;
+
+ synchronized (mManagedCursors) {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (mc.mReleased || mc.mUpdated) {
+ if (!mc.mCursor.requery()) {
+ if (getApplicationInfo().targetSdkVersion
+ >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ throw new IllegalStateException(
+ "trying to requery an already closed cursor "
+ + mc.mCursor);
+ }
+ }
+ mc.mReleased = false;
+ mc.mUpdated = false;
+ }
+ }
+ }
+
+ mCalled = false;
+ mInstrumentation.callActivityOnRestart(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onRestart()");
+ }
+ performStart();
+ }
+ }
+
+ final void performResume() {
+ performRestart();
+
+ mFragments.execPendingActions();
+
+ mLastNonConfigurationInstances = null;
+
+ mCalled = false;
+ // mResumed is set by the instrumentation
+ mInstrumentation.callActivityOnResume(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onResume()");
+ }
+
+ // invisible activities must be finished before onResume() completes
+ if (!mVisibleFromClient && !mFinished) {
+ Log.w(TAG, "An activity without a UI must call finish() before onResume() completes");
+ if (getApplicationInfo().targetSdkVersion
+ > android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
+ throw new IllegalStateException(
+ "Activity " + mComponent.toShortString() +
+ " did not call finish() prior to onResume() completing");
+ }
+ }
+
+ // Now really resume, and install the current status bar and menu.
+ mCalled = false;
+
+ mFragments.dispatchResume();
+ mFragments.execPendingActions();
+
+ onPostResume();
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onPostResume()");
+ }
+ }
+
+ final void performPause() {
+ mDoReportFullyDrawn = false;
+ mFragments.dispatchPause();
+ mCalled = false;
+ onPause();
+ mResumed = false;
+ if (!mCalled && getApplicationInfo().targetSdkVersion
+ >= android.os.Build.VERSION_CODES.GINGERBREAD) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onPause()");
+ }
+ mResumed = false;
+ }
+
+ final void performUserLeaving() {
+ onUserInteraction();
+ onUserLeaveHint();
+ }
+
+ final void performStop(boolean preserveWindow) {
+ mDoReportFullyDrawn = false;
+ mFragments.doLoaderStop(mChangingConfigurations /*retain*/);
+
+ // Disallow entering picture-in-picture after the activity has been stopped
+ mCanEnterPictureInPicture = false;
+
+ if (!mStopped) {
+ if (mWindow != null) {
+ mWindow.closeAllPanels();
+ }
+
+ // If we're preserving the window, don't setStoppedState to true, since we
+ // need the window started immediately again. Stopping the window will
+ // destroys hardware resources and causes flicker.
+ if (!preserveWindow && mToken != null && mParent == null) {
+ WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
+ }
+
+ mFragments.dispatchStop();
+
+ mCalled = false;
+ mInstrumentation.callActivityOnStop(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onStop()");
+ }
+
+ synchronized (mManagedCursors) {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (!mc.mReleased) {
+ mc.mCursor.deactivate();
+ mc.mReleased = true;
+ }
+ }
+ }
+
+ mStopped = true;
+ }
+ mResumed = false;
+ }
+
+ final void performDestroy() {
+ mDestroyed = true;
+ mWindow.destroy();
+ mFragments.dispatchDestroy();
+ onDestroy();
+ mFragments.doLoaderDestroy();
+ if (mVoiceInteractor != null) {
+ mVoiceInteractor.detachActivity();
+ }
+ }
+
+ final void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode,
+ Configuration newConfig) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG,
+ "dispatchMultiWindowModeChanged " + this + ": " + isInMultiWindowMode
+ + " " + newConfig);
+ mFragments.dispatchMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ if (mWindow != null) {
+ mWindow.onMultiWindowModeChanged();
+ }
+ onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+
+ final void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG,
+ "dispatchPictureInPictureModeChanged " + this + ": " + isInPictureInPictureMode
+ + " " + newConfig);
+ mFragments.dispatchPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
+ if (mWindow != null) {
+ mWindow.onPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+ onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
+ }
+
+ /**
+ * @hide
+ */
+ public final boolean isResumed() {
+ return mResumed;
+ }
+
+ private void storeHasCurrentPermissionRequest(Bundle bundle) {
+ if (bundle != null && mHasCurrentPermissionsRequest) {
+ bundle.putBoolean(HAS_CURENT_PERMISSIONS_REQUEST_KEY, true);
+ }
+ }
+
+ private void restoreHasCurrentPermissionRequest(Bundle bundle) {
+ if (bundle != null) {
+ mHasCurrentPermissionsRequest = bundle.getBoolean(
+ HAS_CURENT_PERMISSIONS_REQUEST_KEY, false);
+ }
+ }
+
+ void dispatchActivityResult(String who, int requestCode,
+ int resultCode, Intent data) {
+ if (false) Log.v(
+ TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
+ + ", resCode=" + resultCode + ", data=" + data);
+ mFragments.noteStateNotSaved();
+ if (who == null) {
+ onActivityResult(requestCode, resultCode, data);
+ } else if (who.startsWith(REQUEST_PERMISSIONS_WHO_PREFIX)) {
+ who = who.substring(REQUEST_PERMISSIONS_WHO_PREFIX.length());
+ if (TextUtils.isEmpty(who)) {
+ dispatchRequestPermissionsResult(requestCode, data);
+ } else {
+ Fragment frag = mFragments.findFragmentByWho(who);
+ if (frag != null) {
+ dispatchRequestPermissionsResultToFragment(requestCode, data, frag);
+ }
+ }
+ } else if (who.startsWith("@android:view:")) {
+ ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews(
+ getActivityToken());
+ for (ViewRootImpl viewRoot : views) {
+ if (viewRoot.getView() != null
+ && viewRoot.getView().dispatchActivityResult(
+ who, requestCode, resultCode, data)) {
+ return;
+ }
+ }
+ } else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
+ Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
+ getAutofillManager().onAuthenticationResult(requestCode, resultData);
+ } else {
+ Fragment frag = mFragments.findFragmentByWho(who);
+ if (frag != null) {
+ frag.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ /**
+ * Request to put this Activity in a mode where the user is locked to the
+ * current task.
+ *
+ * This will prevent the user from launching other apps, going to settings, or reaching the
+ * home screen. This does not include those apps whose {@link android.R.attr#lockTaskMode}
+ * values permit launching while locked.
+ *
+ * If {@link DevicePolicyManager#isLockTaskPermitted(String)} returns true or
+ * lockTaskMode=lockTaskModeAlways for this component then the app will go directly into
+ * Lock Task mode. The user will not be able to exit this mode until
+ * {@link Activity#stopLockTask()} is called.
+ *
+ * If {@link DevicePolicyManager#isLockTaskPermitted(String)} returns false
+ * then the system will prompt the user with a dialog requesting permission to enter
+ * this mode. When entered through this method the user can exit at any time through
+ * an action described by the request dialog. Calling stopLockTask will also exit the
+ * mode.
+ *
+ * @see android.R.attr#lockTaskMode
+ */
+ public void startLockTask() {
+ try {
+ ActivityManager.getService().startLockTaskModeByToken(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Allow the user to switch away from the current task.
+ *
+ * Called to end the mode started by {@link Activity#startLockTask}. This
+ * can only be called by activities that have successfully called
+ * startLockTask previously.
+ *
+ * This will allow the user to exit this app and move onto other activities.
+ * <p>Note: This method should only be called when the activity is user-facing. That is,
+ * between onResume() and onPause().
+ * <p>Note: If there are other tasks below this one that are also locked then calling this
+ * method will immediately finish this task and resume the previous locked one, remaining in
+ * lockTask mode.
+ *
+ * @see android.R.attr#lockTaskMode
+ * @see ActivityManager#getLockTaskModeState()
+ */
+ public void stopLockTask() {
+ try {
+ ActivityManager.getService().stopLockTaskMode();
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Shows the user the system defined message for telling the user how to exit
+ * lock task mode. The task containing this activity must be in lock task mode at the time
+ * of this call for the message to be displayed.
+ */
+ public void showLockTaskEscapeMessage() {
+ try {
+ ActivityManager.getService().showLockTaskEscapeMessage(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Check whether the caption on freeform windows is displayed directly on the content.
+ *
+ * @return True if caption is displayed on content, false if it pushes the content down.
+ *
+ * @see #setOverlayWithDecorCaptionEnabled(boolean)
+ * @hide
+ */
+ public boolean isOverlayWithDecorCaptionEnabled() {
+ return mWindow.isOverlayWithDecorCaptionEnabled();
+ }
+
+ /**
+ * Set whether the caption should displayed directly on the content rather than push it down.
+ *
+ * This affects only freeform windows since they display the caption and only the main
+ * window of the activity. The caption is used to drag the window around and also shows
+ * maximize and close action buttons.
+ * @hide
+ */
+ public void setOverlayWithDecorCaptionEnabled(boolean enabled) {
+ mWindow.setOverlayWithDecorCaptionEnabled(enabled);
+ }
+
+ /**
+ * Interface for informing a translucent {@link Activity} once all visible activities below it
+ * have completed drawing. This is necessary only after an {@link Activity} has been made
+ * opaque using {@link Activity#convertFromTranslucent()} and before it has been drawn
+ * translucent again following a call to {@link
+ * Activity#convertToTranslucent(android.app.Activity.TranslucentConversionListener,
+ * ActivityOptions)}
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface TranslucentConversionListener {
+ /**
+ * Callback made following {@link Activity#convertToTranslucent} once all visible Activities
+ * below the top one have been redrawn. Following this callback it is safe to make the top
+ * Activity translucent because the underlying Activity has been drawn.
+ *
+ * @param drawComplete True if the background Activity has drawn itself. False if a timeout
+ * occurred waiting for the Activity to complete drawing.
+ *
+ * @see Activity#convertFromTranslucent()
+ * @see Activity#convertToTranslucent(TranslucentConversionListener, ActivityOptions)
+ */
+ public void onTranslucentConversionComplete(boolean drawComplete);
+ }
+
+ private void dispatchRequestPermissionsResult(int requestCode, Intent data) {
+ mHasCurrentPermissionsRequest = false;
+ // If the package installer crashed we may have not data - best effort.
+ String[] permissions = (data != null) ? data.getStringArrayExtra(
+ PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES) : new String[0];
+ final int[] grantResults = (data != null) ? data.getIntArrayExtra(
+ PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS) : new int[0];
+ onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ private void dispatchRequestPermissionsResultToFragment(int requestCode, Intent data,
+ Fragment fragment) {
+ // If the package installer crashed we may have not data - best effort.
+ String[] permissions = (data != null) ? data.getStringArrayExtra(
+ PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES) : new String[0];
+ final int[] grantResults = (data != null) ? data.getIntArrayExtra(
+ PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS) : new int[0];
+ fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ /** @hide */
+ @Override
+ final public void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
+ Intent fillInIntent) {
+ try {
+ startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX,
+ authenticationId, fillInIntent, 0, 0, null);
+ } catch (IntentSender.SendIntentException e) {
+ Log.e(TAG, "authenticate() failed for intent:" + intent, e);
+ }
+ }
+
+ /** @hide */
+ @Override
+ final public void autofillCallbackResetableStateAvailable() {
+ mAutoFillResetNeeded = true;
+ }
+
+ /** @hide */
+ @Override
+ final public boolean autofillCallbackRequestShowFillUi(@NonNull View anchor, int width,
+ int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) {
+ final boolean wasShowing;
+
+ if (mAutofillPopupWindow == null) {
+ wasShowing = false;
+ mAutofillPopupWindow = new AutofillPopupWindow(presenter);
+ } else {
+ wasShowing = mAutofillPopupWindow.isShowing();
+ }
+ mAutofillPopupWindow.update(anchor, 0, 0, width, height, anchorBounds);
+
+ return !wasShowing && mAutofillPopupWindow.isShowing();
+ }
+
+ /** @hide */
+ @Override
+ final public boolean autofillCallbackRequestHideFillUi() {
+ if (mAutofillPopupWindow == null) {
+ return false;
+ }
+ mAutofillPopupWindow.dismiss();
+ mAutofillPopupWindow = null;
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ @NonNull public View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds) {
+ final View[] views = new View[viewIds.length];
+ final ArrayList<ViewRootImpl> roots =
+ WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
+
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ final View rootView = roots.get(rootNum).getView();
+
+ if (rootView != null) {
+ for (int viewNum = 0; viewNum < viewIds.length; viewNum++) {
+ if (views[viewNum] == null) {
+ views[viewNum] = rootView.findViewByAutofillIdTraversal(
+ viewIds[viewNum]);
+ }
+ }
+ }
+ }
+
+ return views;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable public View findViewByAutofillIdTraversal(int viewId) {
+ final ArrayList<ViewRootImpl> roots =
+ WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ final View rootView = roots.get(rootNum).getView();
+
+ if (rootView != null) {
+ final View view = rootView.findViewByAutofillIdTraversal(viewId);
+ if (view != null) {
+ return view;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** @hide */
+ @Override
+ @NonNull public boolean[] getViewVisibility(@NonNull int[] viewIds) {
+ final boolean[] isVisible = new boolean[viewIds.length];
+ final View views[] = findViewsByAutofillIdTraversal(viewIds);
+
+ for (int i = 0; i < viewIds.length; i++) {
+ View view = views[i];
+ if (view == null) {
+ isVisible[i] = false;
+ continue;
+ }
+
+ isVisible[i] = true;
+
+ // Check if the view is visible by checking all parents
+ while (true) {
+ if (view instanceof DecorView && view.getViewRootImpl() == view.getParent()) {
+ break;
+ }
+
+ if (view.getVisibility() != View.VISIBLE) {
+ isVisible[i] = false;
+ break;
+ }
+
+ if (view.getParent() instanceof View) {
+ view = (View) view.getParent();
+ } else {
+ break;
+ }
+ }
+ }
+
+ return isVisible;
+ }
+
+ /** @hide */
+ @Override
+ public boolean isVisibleForAutofill() {
+ return !mStopped;
+ }
+
+ /**
+ * If set to true, this indicates to the system that it should never take a
+ * screenshot of the activity to be used as a representation while it is not in a started state.
+ * <p>
+ * Note that the system may use the window background of the theme instead to represent
+ * the window when it is not running.
+ * <p>
+ * Also note that in comparison to {@link android.view.WindowManager.LayoutParams#FLAG_SECURE},
+ * this only affects the behavior when the activity's screenshot would be used as a
+ * representation when the activity is not in a started state, i.e. in Overview. The system may
+ * still take screenshots of the activity in other contexts; for example, when the user takes a
+ * screenshot of the entire screen, or when the active
+ * {@link android.service.voice.VoiceInteractionService} requests a screenshot via
+ * {@link android.service.voice.VoiceInteractionSession#SHOW_WITH_SCREENSHOT}.
+ *
+ * @param disable {@code true} to disable preview screenshots; {@code false} otherwise.
+ * @hide
+ */
+ @SystemApi
+ public void setDisablePreviewScreenshots(boolean disable) {
+ try {
+ ActivityManager.getService().setDisablePreviewScreenshots(mToken, disable);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call setDisablePreviewScreenshots", e);
+ }
+ }
+
+ /**
+ * Specifies whether an {@link Activity} should be shown on top of the the lock screen whenever
+ * the lockscreen is up and the activity is resumed. Normally an activity will be transitioned
+ * to the stopped state if it is started while the lockscreen is up, but with this flag set the
+ * activity will remain in the resumed state visible on-top of the lock screen. This value can
+ * be set as a manifest attribute using {@link android.R.attr#showWhenLocked}.
+ *
+ * @param showWhenLocked {@code true} to show the {@link Activity} on top of the lock screen;
+ * {@code false} otherwise.
+ * @see #setTurnScreenOn(boolean)
+ * @see android.R.attr#turnScreenOn
+ * @see android.R.attr#showWhenLocked
+ */
+ public void setShowWhenLocked(boolean showWhenLocked) {
+ try {
+ ActivityManager.getService().setShowWhenLocked(mToken, showWhenLocked);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call setShowWhenLocked", e);
+ }
+ }
+
+ /**
+ * Specifies whether the screen should be turned on when the {@link Activity} is resumed.
+ * Normally an activity will be transitioned to the stopped state if it is started while the
+ * screen if off, but with this flag set the activity will cause the screen to turn on if the
+ * activity will be visible and resumed due to the screen coming on. The screen will not be
+ * turned on if the activity won't be visible after the screen is turned on. This flag is
+ * normally used in conjunction with the {@link android.R.attr#showWhenLocked} flag to make sure
+ * the activity is visible after the screen is turned on when the lockscreen is up. In addition,
+ * if this flag is set and the activity calls {@link
+ * KeyguardManager#requestDismissKeyguard(Activity, KeyguardManager.KeyguardDismissCallback)}
+ * the screen will turn on.
+ *
+ * @param turnScreenOn {@code true} to turn on the screen; {@code false} otherwise.
+ *
+ * @see #setShowWhenLocked(boolean)
+ * @see android.R.attr#turnScreenOn
+ * @see android.R.attr#showWhenLocked
+ */
+ public void setTurnScreenOn(boolean turnScreenOn) {
+ try {
+ ActivityManager.getService().setTurnScreenOn(mToken, turnScreenOn);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call setTurnScreenOn", e);
+ }
+ }
+
+ class HostCallbacks extends FragmentHostCallback<Activity> {
+ public HostCallbacks() {
+ super(Activity.this /*activity*/);
+ }
+
+ @Override
+ public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ Activity.this.dump(prefix, fd, writer, args);
+ }
+
+ @Override
+ public boolean onShouldSaveFragmentState(Fragment fragment) {
+ return !isFinishing();
+ }
+
+ @Override
+ public LayoutInflater onGetLayoutInflater() {
+ final LayoutInflater result = Activity.this.getLayoutInflater();
+ if (onUseFragmentManagerInflaterFactory()) {
+ return result.cloneInContext(Activity.this);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean onUseFragmentManagerInflaterFactory() {
+ // Newer platform versions use the child fragment manager's LayoutInflaterFactory.
+ return getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+ }
+
+ @Override
+ public Activity onGetHost() {
+ return Activity.this;
+ }
+
+ @Override
+ public void onInvalidateOptionsMenu() {
+ Activity.this.invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onStartActivityFromFragment(Fragment fragment, Intent intent, int requestCode,
+ Bundle options) {
+ Activity.this.startActivityFromFragment(fragment, intent, requestCode, options);
+ }
+
+ @Override
+ public void onStartActivityAsUserFromFragment(
+ Fragment fragment, Intent intent, int requestCode, Bundle options,
+ UserHandle user) {
+ Activity.this.startActivityAsUserFromFragment(
+ fragment, intent, requestCode, options, user);
+ }
+
+ @Override
+ public void onStartIntentSenderFromFragment(Fragment fragment, IntentSender intent,
+ int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,
+ int extraFlags, Bundle options) throws IntentSender.SendIntentException {
+ if (mParent == null) {
+ startIntentSenderForResultInner(intent, fragment.mWho, requestCode, fillInIntent,
+ flagsMask, flagsValues, options);
+ } else if (options != null) {
+ mParent.startIntentSenderFromChildFragment(fragment, intent, requestCode,
+ fillInIntent, flagsMask, flagsValues, extraFlags, options);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsFromFragment(Fragment fragment, String[] permissions,
+ int requestCode) {
+ String who = REQUEST_PERMISSIONS_WHO_PREFIX + fragment.mWho;
+ Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
+ startActivityForResult(who, intent, requestCode, null);
+ }
+
+ @Override
+ public boolean onHasWindowAnimations() {
+ return getWindow() != null;
+ }
+
+ @Override
+ public int onGetWindowAnimations() {
+ final Window w = getWindow();
+ return (w == null) ? 0 : w.getAttributes().windowAnimations;
+ }
+
+ @Override
+ public void onAttachFragment(Fragment fragment) {
+ Activity.this.onAttachFragment(fragment);
+ }
+
+ @Nullable
+ @Override
+ public <T extends View> T onFindViewById(int id) {
+ return Activity.this.findViewById(id);
+ }
+
+ @Override
+ public boolean onHasView() {
+ final Window w = getWindow();
+ return (w != null && w.peekDecorView() != null);
+ }
+ }
+}
diff --git a/android/app/ActivityGroup.java b/android/app/ActivityGroup.java
new file mode 100644
index 00000000..78a4dfd4
--- /dev/null
+++ b/android/app/ActivityGroup.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import java.util.HashMap;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * A screen that contains and runs multiple embedded activities.
+ *
+ * @deprecated Use the new {@link Fragment} and {@link FragmentManager} APIs
+ * instead; these are also
+ * available on older platforms through the Android compatibility package.
+ */
+@Deprecated
+public class ActivityGroup extends Activity {
+ private static final String STATES_KEY = "android:states";
+ static final String PARENT_NON_CONFIG_INSTANCE_KEY = "android:parent_non_config_instance";
+
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected LocalActivityManager mLocalActivityManager;
+
+ public ActivityGroup() {
+ this(true);
+ }
+
+ public ActivityGroup(boolean singleActivityMode) {
+ mLocalActivityManager = new LocalActivityManager(this, singleActivityMode);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle states = savedInstanceState != null
+ ? (Bundle) savedInstanceState.getBundle(STATES_KEY) : null;
+ mLocalActivityManager.dispatchCreate(states);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLocalActivityManager.dispatchResume();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Bundle state = mLocalActivityManager.saveInstanceState();
+ if (state != null) {
+ outState.putBundle(STATES_KEY, state);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mLocalActivityManager.dispatchPause(isFinishing());
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mLocalActivityManager.dispatchStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mLocalActivityManager.dispatchDestroy(isFinishing());
+ }
+
+ /**
+ * Returns a HashMap mapping from child activity ids to the return values
+ * from calls to their onRetainNonConfigurationInstance methods.
+ *
+ * {@hide}
+ */
+ @Override
+ public HashMap<String,Object> onRetainNonConfigurationChildInstances() {
+ return mLocalActivityManager.dispatchRetainNonConfigurationInstance();
+ }
+
+ public Activity getCurrentActivity() {
+ return mLocalActivityManager.getCurrentActivity();
+ }
+
+ public final LocalActivityManager getLocalActivityManager() {
+ return mLocalActivityManager;
+ }
+
+ @Override
+ void dispatchActivityResult(String who, int requestCode, int resultCode,
+ Intent data) {
+ if (who != null) {
+ Activity act = mLocalActivityManager.getActivity(who);
+ /*
+ if (false) Log.v(
+ TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
+ + ", resCode=" + resultCode + ", data=" + data
+ + ", rec=" + rec);
+ */
+ if (act != null) {
+ act.onActivityResult(requestCode, resultCode, data);
+ return;
+ }
+ }
+ super.dispatchActivityResult(who, requestCode, resultCode, data);
+ }
+}
+
+
diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java
new file mode 100644
index 00000000..a8665037
--- /dev/null
+++ b/android/app/ActivityManager.java
@@ -0,0 +1,4271 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriPermission;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ConfigurationInfo;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.GraphicBuffer;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.BatteryStats;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.DisplayMetrics;
+import android.util.Singleton;
+import android.util.Size;
+
+import com.android.internal.app.procstats.ProcessStats;
+import com.android.internal.os.RoSystemProperties;
+import com.android.internal.os.TransferPipe;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.LocalServices;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * This class gives information about, and interacts
+ * with, activities, services, and the containing
+ * process.
+ * </p>
+ *
+ * <p>
+ * A number of the methods in this class are for
+ * debugging or informational purposes and they should
+ * not be used to affect any runtime behavior of
+ * your app. These methods are called out as such in
+ * the method level documentation.
+ * </p>
+ *
+ *<p>
+ * Most application developers should not have the need to
+ * use this class, most of whose methods are for specialized
+ * use cases. However, a few methods are more broadly applicable.
+ * For instance, {@link android.app.ActivityManager#isLowRamDevice() isLowRamDevice()}
+ * enables your app to detect whether it is running on a low-memory device,
+ * and behave accordingly.
+ * {@link android.app.ActivityManager#clearApplicationUserData() clearApplicationUserData()}
+ * is for apps with reset-data functionality.
+ * </p>
+ *
+ * <p>
+ * In some special use cases, where an app interacts with
+ * its Task stack, the app may use the
+ * {@link android.app.ActivityManager.AppTask} and
+ * {@link android.app.ActivityManager.RecentTaskInfo} inner
+ * classes. However, in general, the methods in this class should
+ * be used for testing and debugging purposes only.
+ * </p>
+ */
+@SystemService(Context.ACTIVITY_SERVICE)
+public class ActivityManager {
+ private static String TAG = "ActivityManager";
+
+ private static int gMaxRecentTasks = -1;
+
+ private final Context mContext;
+
+ private static volatile boolean sSystemReady = false;
+
+
+ private static final int FIRST_START_FATAL_ERROR_CODE = -100;
+ private static final int LAST_START_FATAL_ERROR_CODE = -1;
+ private static final int FIRST_START_SUCCESS_CODE = 0;
+ private static final int LAST_START_SUCCESS_CODE = 99;
+ private static final int FIRST_START_NON_FATAL_ERROR_CODE = 100;
+ private static final int LAST_START_NON_FATAL_ERROR_CODE = 199;
+
+ static final class UidObserver extends IUidObserver.Stub {
+ final OnUidImportanceListener mListener;
+ final Context mContext;
+
+ UidObserver(OnUidImportanceListener listener, Context clientContext) {
+ mListener = listener;
+ mContext = clientContext;
+ }
+
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ mListener.onUidImportance(uid, RunningAppProcessInfo.procStateToImportanceForClient(
+ procState, mContext));
+ }
+
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
+ mListener.onUidImportance(uid, RunningAppProcessInfo.IMPORTANCE_GONE);
+ }
+
+ @Override
+ public void onUidActive(int uid) {
+ }
+
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {
+ }
+
+ @Override public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ }
+
+ final ArrayMap<OnUidImportanceListener, UidObserver> mImportanceListeners = new ArrayMap<>();
+
+ /**
+ * Defines acceptable types of bugreports.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BUGREPORT_OPTION_FULL,
+ BUGREPORT_OPTION_INTERACTIVE,
+ BUGREPORT_OPTION_REMOTE,
+ BUGREPORT_OPTION_WEAR,
+ BUGREPORT_OPTION_TELEPHONY
+ })
+ public @interface BugreportMode {}
+ /**
+ * Takes a bugreport without user interference (and hence causing less
+ * interference to the system), but includes all sections.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_FULL = 0;
+ /**
+ * Allows user to monitor progress and enter additional data; might not include all
+ * sections.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_INTERACTIVE = 1;
+ /**
+ * Takes a bugreport requested remotely by administrator of the Device Owner app,
+ * not the device's user.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_REMOTE = 2;
+ /**
+ * Takes a bugreport on a wearable device.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_WEAR = 3;
+
+ /**
+ * Takes a lightweight version of bugreport that only includes a few, urgent sections
+ * used to report telephony bugs.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_TELEPHONY = 4;
+
+ /**
+ * <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">{@code
+ * <meta-data>}</a> name for a 'home' Activity that declares a package that is to be
+ * uninstalled in lieu of the declaring one. The package named here must be
+ * signed with the same certificate as the one declaring the {@code <meta-data>}.
+ */
+ public static final String META_HOME_ALTERNATE = "android.app.home.alternate";
+
+ // NOTE: Before adding a new start result, please reference the defined ranges to ensure the
+ // result is properly categorized.
+
+ /**
+ * Result for IActivityManager.startVoiceActivity: active session is currently hidden.
+ * @hide
+ */
+ public static final int START_VOICE_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE;
+
+ /**
+ * Result for IActivityManager.startVoiceActivity: active session does not match
+ * the requesting token.
+ * @hide
+ */
+ public static final int START_VOICE_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 1;
+
+ /**
+ * Result for IActivityManager.startActivity: trying to start a background user
+ * activity that shouldn't be displayed for all users.
+ * @hide
+ */
+ public static final int START_NOT_CURRENT_USER_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 2;
+
+ /**
+ * Result for IActivityManager.startActivity: trying to start an activity under voice
+ * control when that activity does not support the VOICE category.
+ * @hide
+ */
+ public static final int START_NOT_VOICE_COMPATIBLE = FIRST_START_FATAL_ERROR_CODE + 3;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * start had to be canceled.
+ * @hide
+ */
+ public static final int START_CANCELED = FIRST_START_FATAL_ERROR_CODE + 4;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * thing being started is not an activity.
+ * @hide
+ */
+ public static final int START_NOT_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 5;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * caller does not have permission to start the activity.
+ * @hide
+ */
+ public static final int START_PERMISSION_DENIED = FIRST_START_FATAL_ERROR_CODE + 6;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * caller has requested both to forward a result and to receive
+ * a result.
+ * @hide
+ */
+ public static final int START_FORWARD_AND_REQUEST_CONFLICT = FIRST_START_FATAL_ERROR_CODE + 7;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * requested class is not found.
+ * @hide
+ */
+ public static final int START_CLASS_NOT_FOUND = FIRST_START_FATAL_ERROR_CODE + 8;
+
+ /**
+ * Result for IActivityManager.startActivity: an error where the
+ * given Intent could not be resolved to an activity.
+ * @hide
+ */
+ public static final int START_INTENT_NOT_RESOLVED = FIRST_START_FATAL_ERROR_CODE + 9;
+
+ /**
+ * Result for IActivityManager.startAssistantActivity: active session is currently hidden.
+ * @hide
+ */
+ public static final int START_ASSISTANT_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE + 10;
+
+ /**
+ * Result for IActivityManager.startAssistantActivity: active session does not match
+ * the requesting token.
+ * @hide
+ */
+ public static final int START_ASSISTANT_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 11;
+
+ /**
+ * Result for IActivityManaqer.startActivity: the activity was started
+ * successfully as normal.
+ * @hide
+ */
+ public static final int START_SUCCESS = FIRST_START_SUCCESS_CODE;
+
+ /**
+ * Result for IActivityManaqer.startActivity: the caller asked that the Intent not
+ * be executed if it is the recipient, and that is indeed the case.
+ * @hide
+ */
+ public static final int START_RETURN_INTENT_TO_CALLER = FIRST_START_SUCCESS_CODE + 1;
+
+ /**
+ * Result for IActivityManaqer.startActivity: activity wasn't really started, but
+ * a task was simply brought to the foreground.
+ * @hide
+ */
+ public static final int START_TASK_TO_FRONT = FIRST_START_SUCCESS_CODE + 2;
+
+ /**
+ * Result for IActivityManaqer.startActivity: activity wasn't really started, but
+ * the given Intent was given to the existing top activity.
+ * @hide
+ */
+ public static final int START_DELIVERED_TO_TOP = FIRST_START_SUCCESS_CODE + 3;
+
+ /**
+ * Result for IActivityManaqer.startActivity: request was canceled because
+ * app switches are temporarily canceled to ensure the user's last request
+ * (such as pressing home) is performed.
+ * @hide
+ */
+ public static final int START_SWITCHES_CANCELED = FIRST_START_NON_FATAL_ERROR_CODE;
+
+ /**
+ * Result for IActivityManaqer.startActivity: a new activity was attempted to be started
+ * while in Lock Task Mode.
+ * @hide
+ */
+ public static final int START_RETURN_LOCK_TASK_MODE_VIOLATION =
+ FIRST_START_NON_FATAL_ERROR_CODE + 1;
+
+ /**
+ * Result for IActivityManaqer.startActivity: a new activity start was aborted. Never returned
+ * externally.
+ * @hide
+ */
+ public static final int START_ABORTED = FIRST_START_NON_FATAL_ERROR_CODE + 2;
+
+ /**
+ * Flag for IActivityManaqer.startActivity: do special start mode where
+ * a new activity is launched only if it is needed.
+ * @hide
+ */
+ public static final int START_FLAG_ONLY_IF_NEEDED = 1<<0;
+
+ /**
+ * Flag for IActivityManaqer.startActivity: launch the app for
+ * debugging.
+ * @hide
+ */
+ public static final int START_FLAG_DEBUG = 1<<1;
+
+ /**
+ * Flag for IActivityManaqer.startActivity: launch the app for
+ * allocation tracking.
+ * @hide
+ */
+ public static final int START_FLAG_TRACK_ALLOCATION = 1<<2;
+
+ /**
+ * Flag for IActivityManaqer.startActivity: launch the app with
+ * native debugging support.
+ * @hide
+ */
+ public static final int START_FLAG_NATIVE_DEBUGGING = 1<<3;
+
+ /**
+ * Result for IActivityManaqer.broadcastIntent: success!
+ * @hide
+ */
+ public static final int BROADCAST_SUCCESS = 0;
+
+ /**
+ * Result for IActivityManaqer.broadcastIntent: attempt to broadcast
+ * a sticky intent without appropriate permission.
+ * @hide
+ */
+ public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1;
+
+ /**
+ * Result for IActivityManager.broadcastIntent: trying to send a broadcast
+ * to a stopped user. Fail.
+ * @hide
+ */
+ public static final int BROADCAST_FAILED_USER_STOPPED = -2;
+
+ /**
+ * Type for IActivityManaqer.getIntentSender: this PendingIntent is
+ * for a sendBroadcast operation.
+ * @hide
+ */
+ public static final int INTENT_SENDER_BROADCAST = 1;
+
+ /**
+ * Type for IActivityManaqer.getIntentSender: this PendingIntent is
+ * for a startActivity operation.
+ * @hide
+ */
+ public static final int INTENT_SENDER_ACTIVITY = 2;
+
+ /**
+ * Type for IActivityManaqer.getIntentSender: this PendingIntent is
+ * for an activity result operation.
+ * @hide
+ */
+ public static final int INTENT_SENDER_ACTIVITY_RESULT = 3;
+
+ /**
+ * Type for IActivityManaqer.getIntentSender: this PendingIntent is
+ * for a startService operation.
+ * @hide
+ */
+ public static final int INTENT_SENDER_SERVICE = 4;
+
+ /**
+ * Type for IActivityManaqer.getIntentSender: this PendingIntent is
+ * for a startForegroundService operation.
+ * @hide
+ */
+ public static final int INTENT_SENDER_FOREGROUND_SERVICE = 5;
+
+ /** @hide User operation call: success! */
+ public static final int USER_OP_SUCCESS = 0;
+
+ /** @hide User operation call: given user id is not known. */
+ public static final int USER_OP_UNKNOWN_USER = -1;
+
+ /** @hide User operation call: given user id is the current user, can't be stopped. */
+ public static final int USER_OP_IS_CURRENT = -2;
+
+ /** @hide User operation call: system user can't be stopped. */
+ public static final int USER_OP_ERROR_IS_SYSTEM = -3;
+
+ /** @hide User operation call: one of related users cannot be stopped. */
+ public static final int USER_OP_ERROR_RELATED_USERS_CANNOT_STOP = -4;
+
+ /** @hide Not a real process state. */
+ public static final int PROCESS_STATE_UNKNOWN = -1;
+
+ /** @hide Process is a persistent system process. */
+ public static final int PROCESS_STATE_PERSISTENT = 0;
+
+ /** @hide Process is a persistent system process and is doing UI. */
+ public static final int PROCESS_STATE_PERSISTENT_UI = 1;
+
+ /** @hide Process is hosting the current top activities. Note that this covers
+ * all activities that are visible to the user. */
+ public static final int PROCESS_STATE_TOP = 2;
+
+ /** @hide Process is hosting a foreground service due to a system binding. */
+ public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
+
+ /** @hide Process is hosting a foreground service. */
+ public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
+
+ /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
+ public static final int PROCESS_STATE_TOP_SLEEPING = 5;
+
+ /** @hide Process is important to the user, and something they are aware of. */
+ public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
+
+ /** @hide Process is important to the user, but not something they are aware of. */
+ public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
+
+ /** @hide Process is in the background transient so we will try to keep running. */
+ public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 8;
+
+ /** @hide Process is in the background running a backup/restore operation. */
+ public static final int PROCESS_STATE_BACKUP = 9;
+
+ /** @hide Process is in the background, but it can't restore its state so we want
+ * to try to avoid killing it. */
+ public static final int PROCESS_STATE_HEAVY_WEIGHT = 10;
+
+ /** @hide Process is in the background running a service. Unlike oom_adj, this level
+ * is used for both the normal running in background state and the executing
+ * operations state. */
+ public static final int PROCESS_STATE_SERVICE = 11;
+
+ /** @hide Process is in the background running a receiver. Note that from the
+ * perspective of oom_adj receivers run at a higher foreground level, but for our
+ * prioritization here that is not necessary and putting them below services means
+ * many fewer changes in some process states as they receive broadcasts. */
+ public static final int PROCESS_STATE_RECEIVER = 12;
+
+ /** @hide Process is in the background but hosts the home activity. */
+ public static final int PROCESS_STATE_HOME = 13;
+
+ /** @hide Process is in the background but hosts the last shown activity. */
+ public static final int PROCESS_STATE_LAST_ACTIVITY = 14;
+
+ /** @hide Process is being cached for later use and contains activities. */
+ public static final int PROCESS_STATE_CACHED_ACTIVITY = 15;
+
+ /** @hide Process is being cached for later use and is a client of another cached
+ * process that contains activities. */
+ public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 16;
+
+ /** @hide Process is being cached for later use and is empty. */
+ public static final int PROCESS_STATE_CACHED_EMPTY = 17;
+
+ /** @hide Process does not exist. */
+ public static final int PROCESS_STATE_NONEXISTENT = 18;
+
+ /** @hide The lowest process state number */
+ public static final int MIN_PROCESS_STATE = PROCESS_STATE_PERSISTENT;
+
+ /** @hide The highest process state number */
+ public static final int MAX_PROCESS_STATE = PROCESS_STATE_NONEXISTENT;
+
+ /** @hide Should this process state be considered a background state? */
+ public static final boolean isProcStateBackground(int procState) {
+ return procState >= PROCESS_STATE_TRANSIENT_BACKGROUND;
+ }
+
+ /** @hide requestType for assist context: only basic information. */
+ public static final int ASSIST_CONTEXT_BASIC = 0;
+
+ /** @hide requestType for assist context: generate full AssistStructure. */
+ public static final int ASSIST_CONTEXT_FULL = 1;
+
+ /** @hide requestType for assist context: generate full AssistStructure for autofill. */
+ public static final int ASSIST_CONTEXT_AUTOFILL = 2;
+
+ /** @hide Flag for registerUidObserver: report changes in process state. */
+ public static final int UID_OBSERVER_PROCSTATE = 1<<0;
+
+ /** @hide Flag for registerUidObserver: report uid gone. */
+ public static final int UID_OBSERVER_GONE = 1<<1;
+
+ /** @hide Flag for registerUidObserver: report uid has become idle. */
+ public static final int UID_OBSERVER_IDLE = 1<<2;
+
+ /** @hide Flag for registerUidObserver: report uid has become active. */
+ public static final int UID_OBSERVER_ACTIVE = 1<<3;
+
+ /** @hide Flag for registerUidObserver: report uid cached state has changed. */
+ public static final int UID_OBSERVER_CACHED = 1<<4;
+
+ /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: normal free-to-run operation. */
+ public static final int APP_START_MODE_NORMAL = 0;
+
+ /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: delay running until later. */
+ public static final int APP_START_MODE_DELAYED = 1;
+
+ /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: delay running until later, with
+ * rigid errors (throwing exception). */
+ public static final int APP_START_MODE_DELAYED_RIGID = 2;
+
+ /** @hide Mode for {@link IActivityManager#isAppStartModeDisabled}: disable/cancel pending
+ * launches; this is the mode for ephemeral apps. */
+ public static final int APP_START_MODE_DISABLED = 3;
+
+ /**
+ * Lock task mode is not active.
+ */
+ public static final int LOCK_TASK_MODE_NONE = 0;
+
+ /**
+ * Full lock task mode is active.
+ */
+ public static final int LOCK_TASK_MODE_LOCKED = 1;
+
+ /**
+ * App pinning mode is active.
+ */
+ public static final int LOCK_TASK_MODE_PINNED = 2;
+
+ Point mAppTaskThumbnailSize;
+
+ /*package*/ ActivityManager(Context context, Handler handler) {
+ mContext = context;
+ }
+
+ /**
+ * Returns whether the launch was successful.
+ * @hide
+ */
+ public static final boolean isStartResultSuccessful(int result) {
+ return FIRST_START_SUCCESS_CODE <= result && result <= LAST_START_SUCCESS_CODE;
+ }
+
+ /**
+ * Returns whether the launch result was a fatal error.
+ * @hide
+ */
+ public static final boolean isStartResultFatalError(int result) {
+ return FIRST_START_FATAL_ERROR_CODE <= result && result <= LAST_START_FATAL_ERROR_CODE;
+ }
+
+ /**
+ * Screen compatibility mode: the application most always run in
+ * compatibility mode.
+ * @hide
+ */
+ public static final int COMPAT_MODE_ALWAYS = -1;
+
+ /**
+ * Screen compatibility mode: the application can never run in
+ * compatibility mode.
+ * @hide
+ */
+ public static final int COMPAT_MODE_NEVER = -2;
+
+ /**
+ * Screen compatibility mode: unknown.
+ * @hide
+ */
+ public static final int COMPAT_MODE_UNKNOWN = -3;
+
+ /**
+ * Screen compatibility mode: the application currently has compatibility
+ * mode disabled.
+ * @hide
+ */
+ public static final int COMPAT_MODE_DISABLED = 0;
+
+ /**
+ * Screen compatibility mode: the application currently has compatibility
+ * mode enabled.
+ * @hide
+ */
+ public static final int COMPAT_MODE_ENABLED = 1;
+
+ /**
+ * Screen compatibility mode: request to toggle the application's
+ * compatibility mode.
+ * @hide
+ */
+ public static final int COMPAT_MODE_TOGGLE = 2;
+
+ private static final boolean DEVELOPMENT_FORCE_LOW_RAM =
+ SystemProperties.getBoolean("debug.force_low_ram", false);
+
+ /** @hide */
+ @TestApi
+ public static class StackId {
+
+ private StackId() {
+ }
+
+ /** Invalid stack ID. */
+ public static final int INVALID_STACK_ID = -1;
+
+ /** First static stack ID.
+ * @hide */
+ public static final int FIRST_STATIC_STACK_ID = 0;
+
+ /** Home activity stack ID. */
+ public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;
+
+ /** ID of stack where fullscreen activities are normally launched into. */
+ public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
+
+ /** ID of stack where freeform/resized activities are normally launched into. */
+ public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;
+
+ /** ID of stack that occupies a dedicated region of the screen. */
+ public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;
+
+ /** ID of stack that always on top (always visible) when it exist. */
+ public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;
+
+ /** ID of stack that contains the Recents activity. */
+ public static final int RECENTS_STACK_ID = PINNED_STACK_ID + 1;
+
+ /** ID of stack that contains activities launched by the assistant. */
+ public static final int ASSISTANT_STACK_ID = RECENTS_STACK_ID + 1;
+
+ /** Last static stack stack ID.
+ * @hide */
+ public static final int LAST_STATIC_STACK_ID = ASSISTANT_STACK_ID;
+
+ /** Start of ID range used by stacks that are created dynamically.
+ * @hide */
+ public static final int FIRST_DYNAMIC_STACK_ID = LAST_STATIC_STACK_ID + 1;
+
+ // TODO: Figure-out a way to remove this.
+ /** @hide */
+ public static boolean isStaticStack(int stackId) {
+ return stackId >= FIRST_STATIC_STACK_ID && stackId <= LAST_STATIC_STACK_ID;
+ }
+
+ // TODO: It seems this mostly means a stack on a secondary display now. Need to see if
+ // there are other meanings. If not why not just use information from the display?
+ /** @hide */
+ public static boolean isDynamicStack(int stackId) {
+ return stackId >= FIRST_DYNAMIC_STACK_ID;
+ }
+
+ /**
+ * Returns true if dynamic stacks are allowed to be visible behind the input stack.
+ * @hide
+ */
+ // TODO: Figure-out a way to remove.
+ public static boolean isDynamicStacksVisibleBehindAllowed(int stackId) {
+ return stackId == PINNED_STACK_ID || stackId == ASSISTANT_STACK_ID;
+ }
+
+ /**
+ * Returns true if we try to maintain focus in the current stack when the top activity
+ * finishes.
+ * @hide
+ */
+ // TODO: Figure-out a way to remove. Probably isn't needed in the new world...
+ public static boolean keepFocusInStackIfPossible(int stackId) {
+ return stackId == FREEFORM_WORKSPACE_STACK_ID
+ || stackId == DOCKED_STACK_ID || stackId == PINNED_STACK_ID;
+ }
+
+ /**
+ * Returns true if the input stack is affected by drag resizing.
+ * @hide
+ */
+ public static boolean isStackAffectedByDragResizing(int stackId) {
+ return isStaticStack(stackId) && stackId != PINNED_STACK_ID
+ && stackId != ASSISTANT_STACK_ID;
+ }
+
+ /**
+ * Returns true if the windows of tasks being moved to the target stack from the source
+ * stack should be replaced, meaning that window manager will keep the old window around
+ * until the new is ready.
+ * @hide
+ */
+ public static boolean replaceWindowsOnTaskMove(int sourceStackId, int targetStackId) {
+ return sourceStackId == FREEFORM_WORKSPACE_STACK_ID
+ || targetStackId == FREEFORM_WORKSPACE_STACK_ID;
+ }
+
+ /**
+ * Return whether a stackId is a stack that be a backdrop to a translucent activity. These
+ * are generally fullscreen stacks.
+ * @hide
+ */
+ public static boolean isBackdropToTranslucentActivity(int stackId) {
+ return stackId == FULLSCREEN_WORKSPACE_STACK_ID
+ || stackId == ASSISTANT_STACK_ID;
+ }
+
+ /**
+ * Returns true if animation specs should be constructed for app transition that moves
+ * the task to the specified stack.
+ * @hide
+ */
+ public static boolean useAnimationSpecForAppTransition(int stackId) {
+ // TODO: INVALID_STACK_ID is also animated because we don't persist stack id's across
+ // reboots.
+ return stackId == FREEFORM_WORKSPACE_STACK_ID
+ || stackId == FULLSCREEN_WORKSPACE_STACK_ID
+ || stackId == ASSISTANT_STACK_ID
+ || stackId == DOCKED_STACK_ID
+ || stackId == INVALID_STACK_ID;
+ }
+
+ /**
+ * Returns true if activities from stasks in the given {@param stackId} are allowed to
+ * enter picture-in-picture.
+ * @hide
+ */
+ public static boolean isAllowedToEnterPictureInPicture(int stackId) {
+ return stackId != HOME_STACK_ID && stackId != ASSISTANT_STACK_ID &&
+ stackId != RECENTS_STACK_ID;
+ }
+
+ /**
+ * Returns true if the top task in the task is allowed to return home when finished and
+ * there are other tasks in the stack.
+ * @hide
+ */
+ public static boolean allowTopTaskToReturnHome(int stackId) {
+ return stackId != PINNED_STACK_ID;
+ }
+
+ /**
+ * Returns true if the stack should be resized to match the bounds specified by
+ * {@link ActivityOptions#setLaunchBounds} when launching an activity into the stack.
+ * @hide
+ */
+ public static boolean resizeStackWithLaunchBounds(int stackId) {
+ return stackId == PINNED_STACK_ID;
+ }
+
+ /**
+ * Returns true if a window from the specified stack with {@param stackId} are normally
+ * fullscreen, i. e. they can become the top opaque fullscreen window, meaning that it
+ * controls system bars, lockscreen occluded/dismissing state, screen rotation animation,
+ * etc.
+ * @hide
+ */
+ // TODO: What about the other side of docked stack if we move this to WindowConfiguration?
+ public static boolean normallyFullscreenWindows(int stackId) {
+ return stackId != PINNED_STACK_ID && stackId != FREEFORM_WORKSPACE_STACK_ID
+ && stackId != DOCKED_STACK_ID;
+ }
+
+ /**
+ * Returns true if the input stack id should only be present on a device that supports
+ * multi-window mode.
+ * @see android.app.ActivityManager#supportsMultiWindow
+ * @hide
+ */
+ // TODO: What about the other side of docked stack if we move this to WindowConfiguration?
+ public static boolean isMultiWindowStack(int stackId) {
+ return stackId == PINNED_STACK_ID || stackId == FREEFORM_WORKSPACE_STACK_ID
+ || stackId == DOCKED_STACK_ID;
+ }
+
+ /**
+ * Returns true if the input {@param stackId} is HOME_STACK_ID or RECENTS_STACK_ID
+ * @hide
+ */
+ public static boolean isHomeOrRecentsStack(int stackId) {
+ return stackId == HOME_STACK_ID || stackId == RECENTS_STACK_ID;
+ }
+
+ /** Returns true if the input stack and its content can affect the device orientation.
+ * @hide */
+ public static boolean canSpecifyOrientation(int stackId) {
+ return stackId == HOME_STACK_ID
+ || stackId == RECENTS_STACK_ID
+ || stackId == FULLSCREEN_WORKSPACE_STACK_ID
+ || stackId == ASSISTANT_STACK_ID
+ || isDynamicStack(stackId);
+ }
+
+ /** Returns the windowing mode that should be used for this input stack id.
+ * @hide */
+ // TODO: To be removed once we are not using stack id for stuff...
+ public static int getWindowingModeForStackId(int stackId, boolean inSplitScreenMode) {
+ final int windowingMode;
+ switch (stackId) {
+ case FULLSCREEN_WORKSPACE_STACK_ID:
+ case HOME_STACK_ID:
+ case RECENTS_STACK_ID:
+ windowingMode = inSplitScreenMode
+ ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN;
+ break;
+ case ASSISTANT_STACK_ID:
+ windowingMode = WINDOWING_MODE_FULLSCREEN;
+ break;
+ case PINNED_STACK_ID:
+ windowingMode = WINDOWING_MODE_PINNED;
+ break;
+ case DOCKED_STACK_ID:
+ windowingMode = WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+ break;
+ case FREEFORM_WORKSPACE_STACK_ID:
+ windowingMode = WINDOWING_MODE_FREEFORM;
+ break;
+ default :
+ windowingMode = WINDOWING_MODE_UNDEFINED;
+ }
+ return windowingMode;
+ }
+
+ /** Returns the activity type that should be used for this input stack id.
+ * @hide */
+ // TODO: To be removed once we are not using stack id for stuff...
+ public static int getActivityTypeForStackId(int stackId) {
+ final int activityType;
+ switch (stackId) {
+ case HOME_STACK_ID:
+ activityType = ACTIVITY_TYPE_HOME;
+ break;
+ case RECENTS_STACK_ID:
+ activityType = ACTIVITY_TYPE_RECENTS;
+ break;
+ case ASSISTANT_STACK_ID:
+ activityType = ACTIVITY_TYPE_ASSISTANT;
+ break;
+ default :
+ activityType = ACTIVITY_TYPE_STANDARD;
+ }
+ return activityType;
+ }
+ }
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#moveTaskToDockedStack} which
+ * specifies the position of the created docked stack at the top half of the screen if
+ * in portrait mode or at the left half of the screen if in landscape mode.
+ * @hide
+ */
+ public static final int DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT = 0;
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#moveTaskToDockedStack} which
+ * specifies the position of the created docked stack at the bottom half of the screen if
+ * in portrait mode or at the right half of the screen if in landscape mode.
+ * @hide
+ */
+ public static final int DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT = 1;
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} which indicates
+ * that the resize doesn't need to preserve the window, and can be skipped if bounds
+ * is unchanged. This mode is used by window manager in most cases.
+ * @hide
+ */
+ public static final int RESIZE_MODE_SYSTEM = 0;
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} which indicates
+ * that the resize should preserve the window if possible.
+ * @hide
+ */
+ public static final int RESIZE_MODE_PRESERVE_WINDOW = (0x1 << 0);
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} which indicates
+ * that the resize should be performed even if the bounds appears unchanged.
+ * @hide
+ */
+ public static final int RESIZE_MODE_FORCED = (0x1 << 1);
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} used by window
+ * manager during a screen rotation.
+ * @hide
+ */
+ public static final int RESIZE_MODE_SYSTEM_SCREEN_ROTATION = RESIZE_MODE_PRESERVE_WINDOW;
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} used when the
+ * resize is due to a drag action.
+ * @hide
+ */
+ public static final int RESIZE_MODE_USER = RESIZE_MODE_PRESERVE_WINDOW;
+
+ /**
+ * Input parameter to {@link android.app.IActivityManager#resizeTask} which indicates
+ * that the resize should preserve the window if possible, and should not be skipped
+ * even if the bounds is unchanged. Usually used to force a resizing when a drag action
+ * is ending.
+ * @hide
+ */
+ public static final int RESIZE_MODE_USER_FORCED =
+ RESIZE_MODE_PRESERVE_WINDOW | RESIZE_MODE_FORCED;
+
+ /** @hide */
+ public int getFrontActivityScreenCompatMode() {
+ try {
+ return getService().getFrontActivityScreenCompatMode();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setFrontActivityScreenCompatMode(int mode) {
+ try {
+ getService().setFrontActivityScreenCompatMode(mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public int getPackageScreenCompatMode(String packageName) {
+ try {
+ return getService().getPackageScreenCompatMode(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setPackageScreenCompatMode(String packageName, int mode) {
+ try {
+ getService().setPackageScreenCompatMode(packageName, mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public boolean getPackageAskScreenCompat(String packageName) {
+ try {
+ return getService().getPackageAskScreenCompat(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setPackageAskScreenCompat(String packageName, boolean ask) {
+ try {
+ getService().setPackageAskScreenCompat(packageName, ask);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the approximate per-application memory class of the current
+ * device. This gives you an idea of how hard a memory limit you should
+ * impose on your application to let the overall system work best. The
+ * returned value is in megabytes; the baseline Android memory class is
+ * 16 (which happens to be the Java heap limit of those devices); some
+ * device with more memory may return 24 or even higher numbers.
+ */
+ public int getMemoryClass() {
+ return staticGetMemoryClass();
+ }
+
+ /** @hide */
+ static public int staticGetMemoryClass() {
+ // Really brain dead right now -- just take this from the configured
+ // vm heap size, and assume it is in megabytes and thus ends with "m".
+ String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
+ if (vmHeapSize != null && !"".equals(vmHeapSize)) {
+ return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
+ }
+ return staticGetLargeMemoryClass();
+ }
+
+ /**
+ * Return the approximate per-application memory class of the current
+ * device when an application is running with a large heap. This is the
+ * space available for memory-intensive applications; most applications
+ * should not need this amount of memory, and should instead stay with the
+ * {@link #getMemoryClass()} limit. The returned value is in megabytes.
+ * This may be the same size as {@link #getMemoryClass()} on memory
+ * constrained devices, or it may be significantly larger on devices with
+ * a large amount of available RAM.
+ *
+ * <p>The is the size of the application's Dalvik heap if it has
+ * specified <code>android:largeHeap="true"</code> in its manifest.
+ */
+ public int getLargeMemoryClass() {
+ return staticGetLargeMemoryClass();
+ }
+
+ /** @hide */
+ static public int staticGetLargeMemoryClass() {
+ // Really brain dead right now -- just take this from the configured
+ // vm heap size, and assume it is in megabytes and thus ends with "m".
+ String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
+ return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
+ }
+
+ /**
+ * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM
+ * is ultimately up to the device configuration, but currently it generally means
+ * something in the class of a 512MB device with about a 800x480 or less screen.
+ * This is mostly intended to be used by apps to determine whether they should turn
+ * off certain features that require more RAM.
+ */
+ public boolean isLowRamDevice() {
+ return isLowRamDeviceStatic();
+ }
+
+ /** @hide */
+ public static boolean isLowRamDeviceStatic() {
+ return RoSystemProperties.CONFIG_LOW_RAM ||
+ (Build.IS_DEBUGGABLE && DEVELOPMENT_FORCE_LOW_RAM);
+ }
+
+ /**
+ * Returns true if this is a small battery device. Exactly whether a device is considered to be
+ * small battery is ultimately up to the device configuration, but currently it generally means
+ * something in the class of a device with 1000 mAh or less. This is mostly intended to be used
+ * to determine whether certain features should be altered to account for a drastically smaller
+ * battery.
+ * @hide
+ */
+ public static boolean isSmallBatteryDevice() {
+ return RoSystemProperties.CONFIG_SMALL_BATTERY;
+ }
+
+ /**
+ * Used by persistent processes to determine if they are running on a
+ * higher-end device so should be okay using hardware drawing acceleration
+ * (which tends to consume a lot more RAM).
+ * @hide
+ */
+ static public boolean isHighEndGfx() {
+ return !isLowRamDeviceStatic() &&
+ !Resources.getSystem().getBoolean(com.android.internal.R.bool.config_avoidGfxAccel);
+ }
+
+ /**
+ * Return the maximum number of recents entries that we will maintain and show.
+ * @hide
+ */
+ static public int getMaxRecentTasksStatic() {
+ if (gMaxRecentTasks < 0) {
+ return gMaxRecentTasks = isLowRamDeviceStatic() ? 36 : 48;
+ }
+ return gMaxRecentTasks;
+ }
+
+ /**
+ * Return the default limit on the number of recents that an app can make.
+ * @hide
+ */
+ static public int getDefaultAppRecentsLimitStatic() {
+ return getMaxRecentTasksStatic() / 6;
+ }
+
+ /**
+ * Return the maximum limit on the number of recents that an app can make.
+ * @hide
+ */
+ static public int getMaxAppRecentsLimitStatic() {
+ return getMaxRecentTasksStatic() / 2;
+ }
+
+ /**
+ * Returns true if the system supports at least one form of multi-window.
+ * E.g. freeform, split-screen, picture-in-picture.
+ * @hide
+ */
+ static public boolean supportsMultiWindow(Context context) {
+ // On watches, multi-window is used to present essential system UI, and thus it must be
+ // supported regardless of device memory characteristics.
+ boolean isWatch = context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WATCH);
+ return (!isLowRamDeviceStatic() || isWatch)
+ && Resources.getSystem().getBoolean(
+ com.android.internal.R.bool.config_supportsMultiWindow);
+ }
+
+ /**
+ * Returns true if the system supports split screen multi-window.
+ * @hide
+ */
+ static public boolean supportsSplitScreenMultiWindow(Context context) {
+ return supportsMultiWindow(context)
+ && Resources.getSystem().getBoolean(
+ com.android.internal.R.bool.config_supportsSplitScreenMultiWindow);
+ }
+
+ /** @removed */
+ @Deprecated
+ public static int getMaxNumPictureInPictureActions() {
+ return 3;
+ }
+
+ /**
+ * Information you can set and retrieve about the current activity within the recent task list.
+ */
+ public static class TaskDescription implements Parcelable {
+ /** @hide */
+ public static final String ATTR_TASKDESCRIPTION_PREFIX = "task_description_";
+ private static final String ATTR_TASKDESCRIPTIONLABEL =
+ ATTR_TASKDESCRIPTION_PREFIX + "label";
+ private static final String ATTR_TASKDESCRIPTIONCOLOR_PRIMARY =
+ ATTR_TASKDESCRIPTION_PREFIX + "color";
+ private static final String ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND =
+ ATTR_TASKDESCRIPTION_PREFIX + "colorBackground";
+ private static final String ATTR_TASKDESCRIPTIONICONFILENAME =
+ ATTR_TASKDESCRIPTION_PREFIX + "icon_filename";
+
+ private String mLabel;
+ private Bitmap mIcon;
+ private String mIconFilename;
+ private int mColorPrimary;
+ private int mColorBackground;
+ private int mStatusBarColor;
+ private int mNavigationBarColor;
+
+ /**
+ * Creates the TaskDescription to the specified values.
+ *
+ * @param label A label and description of the current state of this task.
+ * @param icon An icon that represents the current state of this task.
+ * @param colorPrimary A color to override the theme's primary color. This color must be
+ * opaque.
+ */
+ public TaskDescription(String label, Bitmap icon, int colorPrimary) {
+ this(label, icon, null, colorPrimary, 0, 0, 0);
+ if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) {
+ throw new RuntimeException("A TaskDescription's primary color should be opaque");
+ }
+ }
+
+ /**
+ * Creates the TaskDescription to the specified values.
+ *
+ * @param label A label and description of the current state of this activity.
+ * @param icon An icon that represents the current state of this activity.
+ */
+ public TaskDescription(String label, Bitmap icon) {
+ this(label, icon, null, 0, 0, 0, 0);
+ }
+
+ /**
+ * Creates the TaskDescription to the specified values.
+ *
+ * @param label A label and description of the current state of this activity.
+ */
+ public TaskDescription(String label) {
+ this(label, null, null, 0, 0, 0, 0);
+ }
+
+ /**
+ * Creates an empty TaskDescription.
+ */
+ public TaskDescription() {
+ this(null, null, null, 0, 0, 0, 0);
+ }
+
+ /** @hide */
+ public TaskDescription(String label, Bitmap icon, String iconFilename, int colorPrimary,
+ int colorBackground, int statusBarColor, int navigationBarColor) {
+ mLabel = label;
+ mIcon = icon;
+ mIconFilename = iconFilename;
+ mColorPrimary = colorPrimary;
+ mColorBackground = colorBackground;
+ mStatusBarColor = statusBarColor;
+ mNavigationBarColor = navigationBarColor;
+ }
+
+ /**
+ * Creates a copy of another TaskDescription.
+ */
+ public TaskDescription(TaskDescription td) {
+ copyFrom(td);
+ }
+
+ /**
+ * Copies this the values from another TaskDescription.
+ * @hide
+ */
+ public void copyFrom(TaskDescription other) {
+ mLabel = other.mLabel;
+ mIcon = other.mIcon;
+ mIconFilename = other.mIconFilename;
+ mColorPrimary = other.mColorPrimary;
+ mColorBackground = other.mColorBackground;
+ mStatusBarColor = other.mStatusBarColor;
+ mNavigationBarColor = other.mNavigationBarColor;
+ }
+
+ /**
+ * Copies this the values from another TaskDescription, but preserves the hidden fields
+ * if they weren't set on {@code other}
+ * @hide
+ */
+ public void copyFromPreserveHiddenFields(TaskDescription other) {
+ mLabel = other.mLabel;
+ mIcon = other.mIcon;
+ mIconFilename = other.mIconFilename;
+ mColorPrimary = other.mColorPrimary;
+ if (other.mColorBackground != 0) {
+ mColorBackground = other.mColorBackground;
+ }
+ if (other.mStatusBarColor != 0) {
+ mStatusBarColor = other.mStatusBarColor;
+ }
+ if (other.mNavigationBarColor != 0) {
+ mNavigationBarColor = other.mNavigationBarColor;
+ }
+ }
+
+ private TaskDescription(Parcel source) {
+ readFromParcel(source);
+ }
+
+ /**
+ * Sets the label for this task description.
+ * @hide
+ */
+ public void setLabel(String label) {
+ mLabel = label;
+ }
+
+ /**
+ * Sets the primary color for this task description.
+ * @hide
+ */
+ public void setPrimaryColor(int primaryColor) {
+ // Ensure that the given color is valid
+ if ((primaryColor != 0) && (Color.alpha(primaryColor) != 255)) {
+ throw new RuntimeException("A TaskDescription's primary color should be opaque");
+ }
+ mColorPrimary = primaryColor;
+ }
+
+ /**
+ * Sets the background color for this task description.
+ * @hide
+ */
+ public void setBackgroundColor(int backgroundColor) {
+ // Ensure that the given color is valid
+ if ((backgroundColor != 0) && (Color.alpha(backgroundColor) != 255)) {
+ throw new RuntimeException("A TaskDescription's background color should be opaque");
+ }
+ mColorBackground = backgroundColor;
+ }
+
+ /**
+ * @hide
+ */
+ public void setStatusBarColor(int statusBarColor) {
+ mStatusBarColor = statusBarColor;
+ }
+
+ /**
+ * @hide
+ */
+ public void setNavigationBarColor(int navigationBarColor) {
+ mNavigationBarColor = navigationBarColor;
+ }
+
+ /**
+ * Sets the icon for this task description.
+ * @hide
+ */
+ public void setIcon(Bitmap icon) {
+ mIcon = icon;
+ }
+
+ /**
+ * Moves the icon bitmap reference from an actual Bitmap to a file containing the
+ * bitmap.
+ * @hide
+ */
+ public void setIconFilename(String iconFilename) {
+ mIconFilename = iconFilename;
+ mIcon = null;
+ }
+
+ /**
+ * @return The label and description of the current state of this task.
+ */
+ public String getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * @return The icon that represents the current state of this task.
+ */
+ public Bitmap getIcon() {
+ if (mIcon != null) {
+ return mIcon;
+ }
+ return loadTaskDescriptionIcon(mIconFilename, UserHandle.myUserId());
+ }
+
+ /** @hide */
+ public String getIconFilename() {
+ return mIconFilename;
+ }
+
+ /** @hide */
+ public Bitmap getInMemoryIcon() {
+ return mIcon;
+ }
+
+ /** @hide */
+ public static Bitmap loadTaskDescriptionIcon(String iconFilename, int userId) {
+ if (iconFilename != null) {
+ try {
+ return getService().getTaskDescriptionIcon(iconFilename,
+ userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The color override on the theme's primary color.
+ */
+ public int getPrimaryColor() {
+ return mColorPrimary;
+ }
+
+ /**
+ * @return The background color.
+ * @hide
+ */
+ public int getBackgroundColor() {
+ return mColorBackground;
+ }
+
+ /**
+ * @hide
+ */
+ public int getStatusBarColor() {
+ return mStatusBarColor;
+ }
+
+ /**
+ * @hide
+ */
+ public int getNavigationBarColor() {
+ return mNavigationBarColor;
+ }
+
+ /** @hide */
+ public void saveToXml(XmlSerializer out) throws IOException {
+ if (mLabel != null) {
+ out.attribute(null, ATTR_TASKDESCRIPTIONLABEL, mLabel);
+ }
+ if (mColorPrimary != 0) {
+ out.attribute(null, ATTR_TASKDESCRIPTIONCOLOR_PRIMARY,
+ Integer.toHexString(mColorPrimary));
+ }
+ if (mColorBackground != 0) {
+ out.attribute(null, ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND,
+ Integer.toHexString(mColorBackground));
+ }
+ if (mIconFilename != null) {
+ out.attribute(null, ATTR_TASKDESCRIPTIONICONFILENAME, mIconFilename);
+ }
+ }
+
+ /** @hide */
+ public void restoreFromXml(String attrName, String attrValue) {
+ if (ATTR_TASKDESCRIPTIONLABEL.equals(attrName)) {
+ setLabel(attrValue);
+ } else if (ATTR_TASKDESCRIPTIONCOLOR_PRIMARY.equals(attrName)) {
+ setPrimaryColor((int) Long.parseLong(attrValue, 16));
+ } else if (ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND.equals(attrName)) {
+ setBackgroundColor((int) Long.parseLong(attrValue, 16));
+ } else if (ATTR_TASKDESCRIPTIONICONFILENAME.equals(attrName)) {
+ setIconFilename(attrValue);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mLabel == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ dest.writeString(mLabel);
+ }
+ if (mIcon == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ }
+ dest.writeInt(mColorPrimary);
+ dest.writeInt(mColorBackground);
+ dest.writeInt(mStatusBarColor);
+ dest.writeInt(mNavigationBarColor);
+ if (mIconFilename == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ dest.writeString(mIconFilename);
+ }
+ }
+
+ public void readFromParcel(Parcel source) {
+ mLabel = source.readInt() > 0 ? source.readString() : null;
+ mIcon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null;
+ mColorPrimary = source.readInt();
+ mColorBackground = source.readInt();
+ mStatusBarColor = source.readInt();
+ mNavigationBarColor = source.readInt();
+ mIconFilename = source.readInt() > 0 ? source.readString() : null;
+ }
+
+ public static final Creator<TaskDescription> CREATOR
+ = new Creator<TaskDescription>() {
+ public TaskDescription createFromParcel(Parcel source) {
+ return new TaskDescription(source);
+ }
+ public TaskDescription[] newArray(int size) {
+ return new TaskDescription[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "TaskDescription Label: " + mLabel + " Icon: " + mIcon +
+ " IconFilename: " + mIconFilename + " colorPrimary: " + mColorPrimary +
+ " colorBackground: " + mColorBackground +
+ " statusBarColor: " + mColorBackground +
+ " navigationBarColor: " + mNavigationBarColor;
+ }
+ }
+
+ /**
+ * Information you can retrieve about tasks that the user has most recently
+ * started or visited.
+ */
+ public static class RecentTaskInfo implements Parcelable {
+ /**
+ * If this task is currently running, this is the identifier for it.
+ * If it is not running, this will be -1.
+ */
+ public int id;
+
+ /**
+ * The true identifier of this task, valid even if it is not running.
+ */
+ public int persistentId;
+
+ /**
+ * The original Intent used to launch the task. You can use this
+ * Intent to re-launch the task (if it is no longer running) or bring
+ * the current task to the front.
+ */
+ public Intent baseIntent;
+
+ /**
+ * If this task was started from an alias, this is the actual
+ * activity component that was initially started; the component of
+ * the baseIntent in this case is the name of the actual activity
+ * implementation that the alias referred to. Otherwise, this is null.
+ */
+ public ComponentName origActivity;
+
+ /**
+ * The actual activity component that started the task.
+ * @hide
+ */
+ @Nullable
+ public ComponentName realActivity;
+
+ /**
+ * Description of the task's last state.
+ */
+ public CharSequence description;
+
+ /**
+ * The id of the ActivityStack this Task was on most recently.
+ * @hide
+ */
+ public int stackId;
+
+ /**
+ * The id of the user the task was running as.
+ * @hide
+ */
+ public int userId;
+
+ /**
+ * The first time this task was active.
+ * @hide
+ */
+ public long firstActiveTime;
+
+ /**
+ * The last time this task was active.
+ * @hide
+ */
+ public long lastActiveTime;
+
+ /**
+ * The recent activity values for the highest activity in the stack to have set the values.
+ * {@link Activity#setTaskDescription(android.app.ActivityManager.TaskDescription)}.
+ */
+ public TaskDescription taskDescription;
+
+ /**
+ * Task affiliation for grouping with other tasks.
+ */
+ public int affiliatedTaskId;
+
+ /**
+ * Task affiliation color of the source task with the affiliated task id.
+ *
+ * @hide
+ */
+ public int affiliatedTaskColor;
+
+ /**
+ * The component launched as the first activity in the task.
+ * This can be considered the "application" of this task.
+ */
+ public ComponentName baseActivity;
+
+ /**
+ * The activity component at the top of the history stack of the task.
+ * This is what the user is currently doing.
+ */
+ public ComponentName topActivity;
+
+ /**
+ * Number of activities in this task.
+ */
+ public int numActivities;
+
+ /**
+ * The bounds of the task.
+ * @hide
+ */
+ public Rect bounds;
+
+ /**
+ * True if the task can go in the docked stack.
+ * @hide
+ */
+ public boolean supportsSplitScreenMultiWindow;
+
+ /**
+ * The resize mode of the task. See {@link ActivityInfo#resizeMode}.
+ * @hide
+ */
+ public int resizeMode;
+
+ public RecentTaskInfo() {
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(id);
+ dest.writeInt(persistentId);
+ if (baseIntent != null) {
+ dest.writeInt(1);
+ baseIntent.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ ComponentName.writeToParcel(origActivity, dest);
+ ComponentName.writeToParcel(realActivity, dest);
+ TextUtils.writeToParcel(description, dest,
+ Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ if (taskDescription != null) {
+ dest.writeInt(1);
+ taskDescription.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(stackId);
+ dest.writeInt(userId);
+ dest.writeLong(firstActiveTime);
+ dest.writeLong(lastActiveTime);
+ dest.writeInt(affiliatedTaskId);
+ dest.writeInt(affiliatedTaskColor);
+ ComponentName.writeToParcel(baseActivity, dest);
+ ComponentName.writeToParcel(topActivity, dest);
+ dest.writeInt(numActivities);
+ if (bounds != null) {
+ dest.writeInt(1);
+ bounds.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0);
+ dest.writeInt(resizeMode);
+ }
+
+ public void readFromParcel(Parcel source) {
+ id = source.readInt();
+ persistentId = source.readInt();
+ baseIntent = source.readInt() > 0 ? Intent.CREATOR.createFromParcel(source) : null;
+ origActivity = ComponentName.readFromParcel(source);
+ realActivity = ComponentName.readFromParcel(source);
+ description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ taskDescription = source.readInt() > 0 ?
+ TaskDescription.CREATOR.createFromParcel(source) : null;
+ stackId = source.readInt();
+ userId = source.readInt();
+ firstActiveTime = source.readLong();
+ lastActiveTime = source.readLong();
+ affiliatedTaskId = source.readInt();
+ affiliatedTaskColor = source.readInt();
+ baseActivity = ComponentName.readFromParcel(source);
+ topActivity = ComponentName.readFromParcel(source);
+ numActivities = source.readInt();
+ bounds = source.readInt() > 0 ?
+ Rect.CREATOR.createFromParcel(source) : null;
+ supportsSplitScreenMultiWindow = source.readInt() == 1;
+ resizeMode = source.readInt();
+ }
+
+ public static final Creator<RecentTaskInfo> CREATOR
+ = new Creator<RecentTaskInfo>() {
+ public RecentTaskInfo createFromParcel(Parcel source) {
+ return new RecentTaskInfo(source);
+ }
+ public RecentTaskInfo[] newArray(int size) {
+ return new RecentTaskInfo[size];
+ }
+ };
+
+ private RecentTaskInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Flag for use with {@link #getRecentTasks}: return all tasks, even those
+ * that have set their
+ * {@link android.content.Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} flag.
+ */
+ public static final int RECENT_WITH_EXCLUDED = 0x0001;
+
+ /**
+ * Provides a list that does not contain any
+ * recent tasks that currently are not available to the user.
+ */
+ public static final int RECENT_IGNORE_UNAVAILABLE = 0x0002;
+
+ /**
+ * Provides a list that contains recent tasks for all
+ * profiles of a user.
+ * @hide
+ */
+ public static final int RECENT_INCLUDE_PROFILES = 0x0004;
+
+ /**
+ * Ignores all tasks that are on the home stack.
+ * @hide
+ */
+ public static final int RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS = 0x0008;
+
+ /**
+ * Ignores the top task in the docked stack.
+ * @hide
+ */
+ public static final int RECENT_INGORE_DOCKED_STACK_TOP_TASK = 0x0010;
+
+ /**
+ * Ignores all tasks that are on the pinned stack.
+ * @hide
+ */
+ public static final int RECENT_INGORE_PINNED_STACK_TASKS = 0x0020;
+
+ /**
+ * <p></p>Return a list of the tasks that the user has recently launched, with
+ * the most recent being first and older ones after in order.
+ *
+ * <p><b>Note: this method is only intended for debugging and presenting
+ * task management user interfaces</b>. This should never be used for
+ * core logic in an application, such as deciding between different
+ * behaviors based on the information found here. Such uses are
+ * <em>not</em> supported, and will likely break in the future. For
+ * example, if multiple applications can be actively running at the
+ * same time, assumptions made about the meaning of the data here for
+ * purposes of control flow will be incorrect.</p>
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method is
+ * no longer available to third party applications: the introduction of
+ * document-centric recents means
+ * it can leak personal information to the caller. For backwards compatibility,
+ * it will still return a small subset of its data: at least the caller's
+ * own tasks (though see {@link #getAppTasks()} for the correct supported
+ * way to retrieve that information), and possibly some other tasks
+ * such as home that are known to not be sensitive.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many tasks the
+ * user has started and the maximum number the system can remember.
+ * @param flags Information about what to return. May be any combination
+ * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}.
+ *
+ * @return Returns a list of RecentTaskInfo records describing each of
+ * the recent tasks.
+ */
+ @Deprecated
+ public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags)
+ throws SecurityException {
+ try {
+ return getService().getRecentTasks(maxNum,
+ flags, UserHandle.myUserId()).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Same as {@link #getRecentTasks(int, int)} but returns the recent tasks for a
+ * specific user. It requires holding
+ * the {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission.
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many tasks the
+ * user has started and the maximum number the system can remember.
+ * @param flags Information about what to return. May be any combination
+ * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}.
+ *
+ * @return Returns a list of RecentTaskInfo records describing each of
+ * the recent tasks. Most recently activated tasks go first.
+ *
+ * @hide
+ */
+ public List<RecentTaskInfo> getRecentTasksForUser(int maxNum, int flags, int userId)
+ throws SecurityException {
+ try {
+ return getService().getRecentTasks(maxNum,
+ flags, userId).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about a particular task that is currently
+ * "running" in the system. Note that a running task does not mean the
+ * given task actually has a process it is actively running in; it simply
+ * means that the user has gone to it and never closed it, but currently
+ * the system may have killed its process and is only holding on to its
+ * last state in order to restart it when the user returns.
+ */
+ public static class RunningTaskInfo implements Parcelable {
+ /**
+ * A unique identifier for this task.
+ */
+ public int id;
+
+ /**
+ * The stack that currently contains this task.
+ * @hide
+ */
+ public int stackId;
+
+ /**
+ * The component launched as the first activity in the task. This can
+ * be considered the "application" of this task.
+ */
+ public ComponentName baseActivity;
+
+ /**
+ * The activity component at the top of the history stack of the task.
+ * This is what the user is currently doing.
+ */
+ public ComponentName topActivity;
+
+ /**
+ * Thumbnail representation of the task's current state. Currently
+ * always null.
+ */
+ public Bitmap thumbnail;
+
+ /**
+ * Description of the task's current state.
+ */
+ public CharSequence description;
+
+ /**
+ * Number of activities in this task.
+ */
+ public int numActivities;
+
+ /**
+ * Number of activities that are currently running (not stopped
+ * and persisted) in this task.
+ */
+ public int numRunning;
+
+ /**
+ * Last time task was run. For sorting.
+ * @hide
+ */
+ public long lastActiveTime;
+
+ /**
+ * True if the task can go in the docked stack.
+ * @hide
+ */
+ public boolean supportsSplitScreenMultiWindow;
+
+ /**
+ * The resize mode of the task. See {@link ActivityInfo#resizeMode}.
+ * @hide
+ */
+ public int resizeMode;
+
+ public RunningTaskInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(id);
+ dest.writeInt(stackId);
+ ComponentName.writeToParcel(baseActivity, dest);
+ ComponentName.writeToParcel(topActivity, dest);
+ if (thumbnail != null) {
+ dest.writeInt(1);
+ thumbnail.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ TextUtils.writeToParcel(description, dest,
+ Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ dest.writeInt(numActivities);
+ dest.writeInt(numRunning);
+ dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0);
+ dest.writeInt(resizeMode);
+ }
+
+ public void readFromParcel(Parcel source) {
+ id = source.readInt();
+ stackId = source.readInt();
+ baseActivity = ComponentName.readFromParcel(source);
+ topActivity = ComponentName.readFromParcel(source);
+ if (source.readInt() != 0) {
+ thumbnail = Bitmap.CREATOR.createFromParcel(source);
+ } else {
+ thumbnail = null;
+ }
+ description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ numActivities = source.readInt();
+ numRunning = source.readInt();
+ supportsSplitScreenMultiWindow = source.readInt() != 0;
+ resizeMode = source.readInt();
+ }
+
+ public static final Creator<RunningTaskInfo> CREATOR = new Creator<RunningTaskInfo>() {
+ public RunningTaskInfo createFromParcel(Parcel source) {
+ return new RunningTaskInfo(source);
+ }
+ public RunningTaskInfo[] newArray(int size) {
+ return new RunningTaskInfo[size];
+ }
+ };
+
+ private RunningTaskInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Get the list of tasks associated with the calling application.
+ *
+ * @return The list of tasks associated with the application making this call.
+ * @throws SecurityException
+ */
+ public List<ActivityManager.AppTask> getAppTasks() {
+ ArrayList<AppTask> tasks = new ArrayList<AppTask>();
+ List<IBinder> appTasks;
+ try {
+ appTasks = getService().getAppTasks(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ int numAppTasks = appTasks.size();
+ for (int i = 0; i < numAppTasks; i++) {
+ tasks.add(new AppTask(IAppTask.Stub.asInterface(appTasks.get(i))));
+ }
+ return tasks;
+ }
+
+ /**
+ * Return the current design dimensions for {@link AppTask} thumbnails, for use
+ * with {@link #addAppTask}.
+ */
+ public Size getAppTaskThumbnailSize() {
+ synchronized (this) {
+ ensureAppTaskThumbnailSizeLocked();
+ return new Size(mAppTaskThumbnailSize.x, mAppTaskThumbnailSize.y);
+ }
+ }
+
+ private void ensureAppTaskThumbnailSizeLocked() {
+ if (mAppTaskThumbnailSize == null) {
+ try {
+ mAppTaskThumbnailSize = getService().getAppTaskThumbnailSize();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Add a new {@link AppTask} for the calling application. This will create a new
+ * recents entry that is added to the <b>end</b> of all existing recents.
+ *
+ * @param activity The activity that is adding the entry. This is used to help determine
+ * the context that the new recents entry will be in.
+ * @param intent The Intent that describes the recents entry. This is the same Intent that
+ * you would have used to launch the activity for it. In generally you will want to set
+ * both {@link Intent#FLAG_ACTIVITY_NEW_DOCUMENT} and
+ * {@link Intent#FLAG_ACTIVITY_RETAIN_IN_RECENTS}; the latter is required since this recents
+ * entry will exist without an activity, so it doesn't make sense to not retain it when
+ * its activity disappears. The given Intent here also must have an explicit ComponentName
+ * set on it.
+ * @param description Optional additional description information.
+ * @param thumbnail Thumbnail to use for the recents entry. Should be the size given by
+ * {@link #getAppTaskThumbnailSize()}. If the bitmap is not that exact size, it will be
+ * recreated in your process, probably in a way you don't like, before the recents entry
+ * is added.
+ *
+ * @return Returns the task id of the newly added app task, or -1 if the add failed. The
+ * most likely cause of failure is that there is no more room for more tasks for your app.
+ */
+ public int addAppTask(@NonNull Activity activity, @NonNull Intent intent,
+ @Nullable TaskDescription description, @NonNull Bitmap thumbnail) {
+ Point size;
+ synchronized (this) {
+ ensureAppTaskThumbnailSizeLocked();
+ size = mAppTaskThumbnailSize;
+ }
+ final int tw = thumbnail.getWidth();
+ final int th = thumbnail.getHeight();
+ if (tw != size.x || th != size.y) {
+ Bitmap bm = Bitmap.createBitmap(size.x, size.y, thumbnail.getConfig());
+
+ // Use ScaleType.CENTER_CROP, except we leave the top edge at the top.
+ float scale;
+ float dx = 0, dy = 0;
+ if (tw * size.x > size.y * th) {
+ scale = (float) size.x / (float) th;
+ dx = (size.y - tw * scale) * 0.5f;
+ } else {
+ scale = (float) size.y / (float) tw;
+ dy = (size.x - th * scale) * 0.5f;
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ matrix.postTranslate((int) (dx + 0.5f), 0);
+
+ Canvas canvas = new Canvas(bm);
+ canvas.drawBitmap(thumbnail, matrix, null);
+ canvas.setBitmap(null);
+
+ thumbnail = bm;
+ }
+ if (description == null) {
+ description = new TaskDescription();
+ }
+ try {
+ return getService().addAppTask(activity.getActivityToken(),
+ intent, description, thumbnail);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return a list of the tasks that are currently running, with
+ * the most recent being first and older ones after in order. Note that
+ * "running" does not mean any of the task's code is currently loaded or
+ * activity -- the task may have been frozen by the system, so that it
+ * can be restarted in its previous state when next brought to the
+ * foreground.
+ *
+ * <p><b>Note: this method is only intended for debugging and presenting
+ * task management user interfaces</b>. This should never be used for
+ * core logic in an application, such as deciding between different
+ * behaviors based on the information found here. Such uses are
+ * <em>not</em> supported, and will likely break in the future. For
+ * example, if multiple applications can be actively running at the
+ * same time, assumptions made about the meaning of the data here for
+ * purposes of control flow will be incorrect.</p>
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method
+ * is no longer available to third party
+ * applications: the introduction of document-centric recents means
+ * it can leak person information to the caller. For backwards compatibility,
+ * it will still return a small subset of its data: at least the caller's
+ * own tasks, and possibly some other tasks
+ * such as home that are known to not be sensitive.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many tasks the
+ * user has started.
+ *
+ * @return Returns a list of RunningTaskInfo records describing each of
+ * the running tasks.
+ */
+ @Deprecated
+ public List<RunningTaskInfo> getRunningTasks(int maxNum)
+ throws SecurityException {
+ try {
+ return getService().getTasks(maxNum, 0);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Completely remove the given task.
+ *
+ * @param taskId Identifier of the task to be removed.
+ * @return Returns true if the given task was found and removed.
+ *
+ * @hide
+ */
+ public boolean removeTask(int taskId) throws SecurityException {
+ try {
+ return getService().removeTask(taskId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Represents a task snapshot.
+ * @hide
+ */
+ public static class TaskSnapshot implements Parcelable {
+
+ private final GraphicBuffer mSnapshot;
+ private final int mOrientation;
+ private final Rect mContentInsets;
+ private final boolean mReducedResolution;
+ private final float mScale;
+
+ public TaskSnapshot(GraphicBuffer snapshot, int orientation, Rect contentInsets,
+ boolean reducedResolution, float scale) {
+ mSnapshot = snapshot;
+ mOrientation = orientation;
+ mContentInsets = new Rect(contentInsets);
+ mReducedResolution = reducedResolution;
+ mScale = scale;
+ }
+
+ private TaskSnapshot(Parcel source) {
+ mSnapshot = source.readParcelable(null /* classLoader */);
+ mOrientation = source.readInt();
+ mContentInsets = source.readParcelable(null /* classLoader */);
+ mReducedResolution = source.readBoolean();
+ mScale = source.readFloat();
+ }
+
+ /**
+ * @return The graphic buffer representing the screenshot.
+ */
+ public GraphicBuffer getSnapshot() {
+ return mSnapshot;
+ }
+
+ /**
+ * @return The screen orientation the screenshot was taken in.
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * @return The system/content insets on the snapshot. These can be clipped off in order to
+ * remove any areas behind system bars in the snapshot.
+ */
+ public Rect getContentInsets() {
+ return mContentInsets;
+ }
+
+ /**
+ * @return Whether this snapshot is a down-sampled version of the full resolution.
+ */
+ public boolean isReducedResolution() {
+ return mReducedResolution;
+ }
+
+ /**
+ * @return The scale this snapshot was taken in.
+ */
+ public float getScale() {
+ return mScale;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mSnapshot, 0);
+ dest.writeInt(mOrientation);
+ dest.writeParcelable(mContentInsets, 0);
+ dest.writeBoolean(mReducedResolution);
+ dest.writeFloat(mScale);
+ }
+
+ @Override
+ public String toString() {
+ return "TaskSnapshot{mSnapshot=" + mSnapshot + " mOrientation=" + mOrientation
+ + " mContentInsets=" + mContentInsets.toShortString()
+ + " mReducedResolution=" + mReducedResolution + " mScale=" + mScale;
+ }
+
+ public static final Creator<TaskSnapshot> CREATOR = new Creator<TaskSnapshot>() {
+ public TaskSnapshot createFromParcel(Parcel source) {
+ return new TaskSnapshot(source);
+ }
+ public TaskSnapshot[] newArray(int size) {
+ return new TaskSnapshot[size];
+ }
+ };
+ }
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "MOVE_TASK_" }, value = {
+ MOVE_TASK_WITH_HOME,
+ MOVE_TASK_NO_USER_ACTION,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MoveTaskFlags {}
+
+ /**
+ * Flag for {@link #moveTaskToFront(int, int)}: also move the "home"
+ * activity along with the task, so it is positioned immediately behind
+ * the task.
+ */
+ public static final int MOVE_TASK_WITH_HOME = 0x00000001;
+
+ /**
+ * Flag for {@link #moveTaskToFront(int, int)}: don't count this as a
+ * user-instigated action, so the current activity will not receive a
+ * hint that the user is leaving.
+ */
+ public static final int MOVE_TASK_NO_USER_ACTION = 0x00000002;
+
+ /**
+ * Equivalent to calling {@link #moveTaskToFront(int, int, Bundle)}
+ * with a null options argument.
+ *
+ * @param taskId The identifier of the task to be moved, as found in
+ * {@link RunningTaskInfo} or {@link RecentTaskInfo}.
+ * @param flags Additional operational flags.
+ */
+ @RequiresPermission(android.Manifest.permission.REORDER_TASKS)
+ public void moveTaskToFront(int taskId, @MoveTaskFlags int flags) {
+ moveTaskToFront(taskId, flags, null);
+ }
+
+ /**
+ * Ask that the task associated with a given task ID be moved to the
+ * front of the stack, so it is now visible to the user.
+ *
+ * @param taskId The identifier of the task to be moved, as found in
+ * {@link RunningTaskInfo} or {@link RecentTaskInfo}.
+ * @param flags Additional operational flags.
+ * @param options Additional options for the operation, either null or
+ * as per {@link Context#startActivity(Intent, android.os.Bundle)
+ * Context.startActivity(Intent, Bundle)}.
+ */
+ @RequiresPermission(android.Manifest.permission.REORDER_TASKS)
+ public void moveTaskToFront(int taskId, @MoveTaskFlags int flags, Bundle options) {
+ try {
+ getService().moveTaskToFront(taskId, flags, options);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about a particular Service that is
+ * currently running in the system.
+ */
+ public static class RunningServiceInfo implements Parcelable {
+ /**
+ * The service component.
+ */
+ public ComponentName service;
+
+ /**
+ * If non-zero, this is the process the service is running in.
+ */
+ public int pid;
+
+ /**
+ * The UID that owns this service.
+ */
+ public int uid;
+
+ /**
+ * The name of the process this service runs in.
+ */
+ public String process;
+
+ /**
+ * Set to true if the service has asked to run as a foreground process.
+ */
+ public boolean foreground;
+
+ /**
+ * The time when the service was first made active, either by someone
+ * starting or binding to it. This
+ * is in units of {@link android.os.SystemClock#elapsedRealtime()}.
+ */
+ public long activeSince;
+
+ /**
+ * Set to true if this service has been explicitly started.
+ */
+ public boolean started;
+
+ /**
+ * Number of clients connected to the service.
+ */
+ public int clientCount;
+
+ /**
+ * Number of times the service's process has crashed while the service
+ * is running.
+ */
+ public int crashCount;
+
+ /**
+ * The time when there was last activity in the service (either
+ * explicit requests to start it or clients binding to it). This
+ * is in units of {@link android.os.SystemClock#uptimeMillis()}.
+ */
+ public long lastActivityTime;
+
+ /**
+ * If non-zero, this service is not currently running, but scheduled to
+ * restart at the given time.
+ */
+ public long restarting;
+
+ /**
+ * Bit for {@link #flags}: set if this service has been
+ * explicitly started.
+ */
+ public static final int FLAG_STARTED = 1<<0;
+
+ /**
+ * Bit for {@link #flags}: set if the service has asked to
+ * run as a foreground process.
+ */
+ public static final int FLAG_FOREGROUND = 1<<1;
+
+ /**
+ * Bit for {@link #flags}: set if the service is running in a
+ * core system process.
+ */
+ public static final int FLAG_SYSTEM_PROCESS = 1<<2;
+
+ /**
+ * Bit for {@link #flags}: set if the service is running in a
+ * persistent process.
+ */
+ public static final int FLAG_PERSISTENT_PROCESS = 1<<3;
+
+ /**
+ * Running flags.
+ */
+ public int flags;
+
+ /**
+ * For special services that are bound to by system code, this is
+ * the package that holds the binding.
+ */
+ public String clientPackage;
+
+ /**
+ * For special services that are bound to by system code, this is
+ * a string resource providing a user-visible label for who the
+ * client is.
+ */
+ public int clientLabel;
+
+ public RunningServiceInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ ComponentName.writeToParcel(service, dest);
+ dest.writeInt(pid);
+ dest.writeInt(uid);
+ dest.writeString(process);
+ dest.writeInt(foreground ? 1 : 0);
+ dest.writeLong(activeSince);
+ dest.writeInt(started ? 1 : 0);
+ dest.writeInt(clientCount);
+ dest.writeInt(crashCount);
+ dest.writeLong(lastActivityTime);
+ dest.writeLong(restarting);
+ dest.writeInt(this.flags);
+ dest.writeString(clientPackage);
+ dest.writeInt(clientLabel);
+ }
+
+ public void readFromParcel(Parcel source) {
+ service = ComponentName.readFromParcel(source);
+ pid = source.readInt();
+ uid = source.readInt();
+ process = source.readString();
+ foreground = source.readInt() != 0;
+ activeSince = source.readLong();
+ started = source.readInt() != 0;
+ clientCount = source.readInt();
+ crashCount = source.readInt();
+ lastActivityTime = source.readLong();
+ restarting = source.readLong();
+ flags = source.readInt();
+ clientPackage = source.readString();
+ clientLabel = source.readInt();
+ }
+
+ public static final Creator<RunningServiceInfo> CREATOR = new Creator<RunningServiceInfo>() {
+ public RunningServiceInfo createFromParcel(Parcel source) {
+ return new RunningServiceInfo(source);
+ }
+ public RunningServiceInfo[] newArray(int size) {
+ return new RunningServiceInfo[size];
+ }
+ };
+
+ private RunningServiceInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Return a list of the services that are currently running.
+ *
+ * <p><b>Note: this method is only intended for debugging or implementing
+ * service management type user interfaces.</b></p>
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#O}, this method
+ * is no longer available to third party applications. For backwards compatibility,
+ * it will still return the caller's own services.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many services
+ * are running.
+ *
+ * @return Returns a list of RunningServiceInfo records describing each of
+ * the running tasks.
+ */
+ @Deprecated
+ public List<RunningServiceInfo> getRunningServices(int maxNum)
+ throws SecurityException {
+ try {
+ return getService()
+ .getServices(maxNum, 0);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns a PendingIntent you can start to show a control panel for the
+ * given running service. If the service does not have a control panel,
+ * null is returned.
+ */
+ public PendingIntent getRunningServiceControlPanel(ComponentName service)
+ throws SecurityException {
+ try {
+ return getService()
+ .getRunningServiceControlPanel(service);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about the available memory through
+ * {@link ActivityManager#getMemoryInfo}.
+ */
+ public static class MemoryInfo implements Parcelable {
+ /**
+ * The available memory on the system. This number should not
+ * be considered absolute: due to the nature of the kernel, a significant
+ * portion of this memory is actually in use and needed for the overall
+ * system to run well.
+ */
+ public long availMem;
+
+ /**
+ * The total memory accessible by the kernel. This is basically the
+ * RAM size of the device, not including below-kernel fixed allocations
+ * like DMA buffers, RAM for the baseband CPU, etc.
+ */
+ public long totalMem;
+
+ /**
+ * The threshold of {@link #availMem} at which we consider memory to be
+ * low and start killing background services and other non-extraneous
+ * processes.
+ */
+ public long threshold;
+
+ /**
+ * Set to true if the system considers itself to currently be in a low
+ * memory situation.
+ */
+ public boolean lowMemory;
+
+ /** @hide */
+ public long hiddenAppThreshold;
+ /** @hide */
+ public long secondaryServerThreshold;
+ /** @hide */
+ public long visibleAppThreshold;
+ /** @hide */
+ public long foregroundAppThreshold;
+
+ public MemoryInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(availMem);
+ dest.writeLong(totalMem);
+ dest.writeLong(threshold);
+ dest.writeInt(lowMemory ? 1 : 0);
+ dest.writeLong(hiddenAppThreshold);
+ dest.writeLong(secondaryServerThreshold);
+ dest.writeLong(visibleAppThreshold);
+ dest.writeLong(foregroundAppThreshold);
+ }
+
+ public void readFromParcel(Parcel source) {
+ availMem = source.readLong();
+ totalMem = source.readLong();
+ threshold = source.readLong();
+ lowMemory = source.readInt() != 0;
+ hiddenAppThreshold = source.readLong();
+ secondaryServerThreshold = source.readLong();
+ visibleAppThreshold = source.readLong();
+ foregroundAppThreshold = source.readLong();
+ }
+
+ public static final Creator<MemoryInfo> CREATOR
+ = new Creator<MemoryInfo>() {
+ public MemoryInfo createFromParcel(Parcel source) {
+ return new MemoryInfo(source);
+ }
+ public MemoryInfo[] newArray(int size) {
+ return new MemoryInfo[size];
+ }
+ };
+
+ private MemoryInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Return general information about the memory state of the system. This
+ * can be used to help decide how to manage your own memory, though note
+ * that polling is not recommended and
+ * {@link android.content.ComponentCallbacks2#onTrimMemory(int)
+ * ComponentCallbacks2.onTrimMemory(int)} is the preferred way to do this.
+ * Also see {@link #getMyMemoryState} for how to retrieve the current trim
+ * level of your process as needed, which gives a better hint for how to
+ * manage its memory.
+ */
+ public void getMemoryInfo(MemoryInfo outInfo) {
+ try {
+ getService().getMemoryInfo(outInfo);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about an ActivityStack in the system.
+ * @hide
+ */
+ public static class StackInfo implements Parcelable {
+ public int stackId;
+ public Rect bounds = new Rect();
+ public int[] taskIds;
+ public String[] taskNames;
+ public Rect[] taskBounds;
+ public int[] taskUserIds;
+ public ComponentName topActivity;
+ public int displayId;
+ public int userId;
+ public boolean visible;
+ // Index of the stack in the display's stack list, can be used for comparison of stack order
+ public int position;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(stackId);
+ dest.writeInt(bounds.left);
+ dest.writeInt(bounds.top);
+ dest.writeInt(bounds.right);
+ dest.writeInt(bounds.bottom);
+ dest.writeIntArray(taskIds);
+ dest.writeStringArray(taskNames);
+ final int boundsCount = taskBounds == null ? 0 : taskBounds.length;
+ dest.writeInt(boundsCount);
+ for (int i = 0; i < boundsCount; i++) {
+ dest.writeInt(taskBounds[i].left);
+ dest.writeInt(taskBounds[i].top);
+ dest.writeInt(taskBounds[i].right);
+ dest.writeInt(taskBounds[i].bottom);
+ }
+ dest.writeIntArray(taskUserIds);
+ dest.writeInt(displayId);
+ dest.writeInt(userId);
+ dest.writeInt(visible ? 1 : 0);
+ dest.writeInt(position);
+ if (topActivity != null) {
+ dest.writeInt(1);
+ topActivity.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ public void readFromParcel(Parcel source) {
+ stackId = source.readInt();
+ bounds = new Rect(
+ source.readInt(), source.readInt(), source.readInt(), source.readInt());
+ taskIds = source.createIntArray();
+ taskNames = source.createStringArray();
+ final int boundsCount = source.readInt();
+ if (boundsCount > 0) {
+ taskBounds = new Rect[boundsCount];
+ for (int i = 0; i < boundsCount; i++) {
+ taskBounds[i] = new Rect();
+ taskBounds[i].set(
+ source.readInt(), source.readInt(), source.readInt(), source.readInt());
+ }
+ } else {
+ taskBounds = null;
+ }
+ taskUserIds = source.createIntArray();
+ displayId = source.readInt();
+ userId = source.readInt();
+ visible = source.readInt() > 0;
+ position = source.readInt();
+ if (source.readInt() > 0) {
+ topActivity = ComponentName.readFromParcel(source);
+ }
+ }
+
+ public static final Creator<StackInfo> CREATOR = new Creator<StackInfo>() {
+ @Override
+ public StackInfo createFromParcel(Parcel source) {
+ return new StackInfo(source);
+ }
+ @Override
+ public StackInfo[] newArray(int size) {
+ return new StackInfo[size];
+ }
+ };
+
+ public StackInfo() {
+ }
+
+ private StackInfo(Parcel source) {
+ readFromParcel(source);
+ }
+
+ public String toString(String prefix) {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append(prefix); sb.append("Stack id="); sb.append(stackId);
+ sb.append(" bounds="); sb.append(bounds.toShortString());
+ sb.append(" displayId="); sb.append(displayId);
+ sb.append(" userId="); sb.append(userId);
+ sb.append("\n");
+ prefix = prefix + " ";
+ for (int i = 0; i < taskIds.length; ++i) {
+ sb.append(prefix); sb.append("taskId="); sb.append(taskIds[i]);
+ sb.append(": "); sb.append(taskNames[i]);
+ if (taskBounds != null) {
+ sb.append(" bounds="); sb.append(taskBounds[i].toShortString());
+ }
+ sb.append(" userId=").append(taskUserIds[i]);
+ sb.append(" visible=").append(visible);
+ if (topActivity != null) {
+ sb.append(" topActivity=").append(topActivity);
+ }
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return toString("");
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RequiresPermission(anyOf={Manifest.permission.CLEAR_APP_USER_DATA,
+ Manifest.permission.ACCESS_INSTANT_APPS})
+ public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) {
+ try {
+ return getService().clearApplicationUserData(packageName,
+ observer, UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Permits an application to erase its own data from disk. This is equivalent to
+ * the user choosing to clear the app's data from within the device settings UI. It
+ * erases all dynamic data associated with the app -- its private data and data in its
+ * private area on external storage -- but does not remove the installed application
+ * itself, nor any OBB files. It also revokes all runtime permissions that the app has acquired,
+ * clears all notifications and removes all Uri grants related to this application.
+ *
+ * @return {@code true} if the application successfully requested that the application's
+ * data be erased; {@code false} otherwise.
+ */
+ public boolean clearApplicationUserData() {
+ return clearApplicationUserData(mContext.getPackageName(), null);
+ }
+
+
+ /**
+ * Permits an application to get the persistent URI permissions granted to another.
+ *
+ * <p>Typically called by Settings.
+ *
+ * @param packageName application to look for the granted permissions
+ * @return list of granted URI permissions
+ *
+ * @hide
+ */
+ public ParceledListSlice<UriPermission> getGrantedUriPermissions(String packageName) {
+ try {
+ return getService().getGrantedUriPermissions(packageName,
+ UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Permits an application to clear the persistent URI permissions granted to another.
+ *
+ * <p>Typically called by Settings.
+ *
+ * @param packageName application to clear its granted permissions
+ *
+ * @hide
+ */
+ public void clearGrantedUriPermissions(String packageName) {
+ try {
+ getService().clearGrantedUriPermissions(packageName,
+ UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about any processes that are in an error condition.
+ */
+ public static class ProcessErrorStateInfo implements Parcelable {
+ /**
+ * Condition codes
+ */
+ public static final int NO_ERROR = 0;
+ public static final int CRASHED = 1;
+ public static final int NOT_RESPONDING = 2;
+
+ /**
+ * The condition that the process is in.
+ */
+ public int condition;
+
+ /**
+ * The process name in which the crash or error occurred.
+ */
+ public String processName;
+
+ /**
+ * The pid of this process; 0 if none
+ */
+ public int pid;
+
+ /**
+ * The kernel user-ID that has been assigned to this process;
+ * currently this is not a unique ID (multiple applications can have
+ * the same uid).
+ */
+ public int uid;
+
+ /**
+ * The activity name associated with the error, if known. May be null.
+ */
+ public String tag;
+
+ /**
+ * A short message describing the error condition.
+ */
+ public String shortMsg;
+
+ /**
+ * A long message describing the error condition.
+ */
+ public String longMsg;
+
+ /**
+ * The stack trace where the error originated. May be null.
+ */
+ public String stackTrace;
+
+ /**
+ * to be deprecated: This value will always be null.
+ */
+ public byte[] crashData = null;
+
+ public ProcessErrorStateInfo() {
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(condition);
+ dest.writeString(processName);
+ dest.writeInt(pid);
+ dest.writeInt(uid);
+ dest.writeString(tag);
+ dest.writeString(shortMsg);
+ dest.writeString(longMsg);
+ dest.writeString(stackTrace);
+ }
+
+ public void readFromParcel(Parcel source) {
+ condition = source.readInt();
+ processName = source.readString();
+ pid = source.readInt();
+ uid = source.readInt();
+ tag = source.readString();
+ shortMsg = source.readString();
+ longMsg = source.readString();
+ stackTrace = source.readString();
+ }
+
+ public static final Creator<ProcessErrorStateInfo> CREATOR =
+ new Creator<ProcessErrorStateInfo>() {
+ public ProcessErrorStateInfo createFromParcel(Parcel source) {
+ return new ProcessErrorStateInfo(source);
+ }
+ public ProcessErrorStateInfo[] newArray(int size) {
+ return new ProcessErrorStateInfo[size];
+ }
+ };
+
+ private ProcessErrorStateInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Returns a list of any processes that are currently in an error condition. The result
+ * will be null if all processes are running properly at this time.
+ *
+ * @return Returns a list of ProcessErrorStateInfo records, or null if there are no
+ * current error conditions (it will not return an empty list). This list ordering is not
+ * specified.
+ */
+ public List<ProcessErrorStateInfo> getProcessesInErrorState() {
+ try {
+ return getService().getProcessesInErrorState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Information you can retrieve about a running process.
+ */
+ public static class RunningAppProcessInfo implements Parcelable {
+ /**
+ * The name of the process that this object is associated with
+ */
+ public String processName;
+
+ /**
+ * The pid of this process; 0 if none
+ */
+ public int pid;
+
+ /**
+ * The user id of this process.
+ */
+ public int uid;
+
+ /**
+ * All packages that have been loaded into the process.
+ */
+ public String pkgList[];
+
+ /**
+ * Constant for {@link #flags}: this is an app that is unable to
+ * correctly save its state when going to the background,
+ * so it can not be killed while in the background.
+ * @hide
+ */
+ public static final int FLAG_CANT_SAVE_STATE = 1<<0;
+
+ /**
+ * Constant for {@link #flags}: this process is associated with a
+ * persistent system app.
+ * @hide
+ */
+ public static final int FLAG_PERSISTENT = 1<<1;
+
+ /**
+ * Constant for {@link #flags}: this process is associated with a
+ * persistent system app.
+ * @hide
+ */
+ public static final int FLAG_HAS_ACTIVITIES = 1<<2;
+
+ /**
+ * Flags of information. May be any of
+ * {@link #FLAG_CANT_SAVE_STATE}.
+ * @hide
+ */
+ public int flags;
+
+ /**
+ * Last memory trim level reported to the process: corresponds to
+ * the values supplied to {@link android.content.ComponentCallbacks2#onTrimMemory(int)
+ * ComponentCallbacks2.onTrimMemory(int)}.
+ */
+ public int lastTrimLevel;
+
+ /** @hide */
+ @IntDef(prefix = { "IMPORTANCE_" }, value = {
+ IMPORTANCE_FOREGROUND,
+ IMPORTANCE_FOREGROUND_SERVICE,
+ IMPORTANCE_TOP_SLEEPING,
+ IMPORTANCE_VISIBLE,
+ IMPORTANCE_PERCEPTIBLE,
+ IMPORTANCE_CANT_SAVE_STATE,
+ IMPORTANCE_SERVICE,
+ IMPORTANCE_CACHED,
+ IMPORTANCE_GONE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Importance {}
+
+ /**
+ * Constant for {@link #importance}: This process is running the
+ * foreground UI; that is, it is the thing currently at the top of the screen
+ * that the user is interacting with.
+ */
+ public static final int IMPORTANCE_FOREGROUND = 100;
+
+ /**
+ * Constant for {@link #importance}: This process is running a foreground
+ * service, for example to perform music playback even while the user is
+ * not immediately in the app. This generally indicates that the process
+ * is doing something the user actively cares about.
+ */
+ public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
+
+ /**
+ * Constant for {@link #importance}: This process is running the foreground
+ * UI, but the device is asleep so it is not visible to the user. This means
+ * the user is not really aware of the process, because they can not see or
+ * interact with it, but it is quite important because it what they expect to
+ * return to once unlocking the device.
+ */
+ public static final int IMPORTANCE_TOP_SLEEPING = 150;
+
+ /**
+ * Constant for {@link #importance}: This process is running something
+ * that is actively visible to the user, though not in the immediate
+ * foreground. This may be running a window that is behind the current
+ * foreground (so paused and with its state saved, not interacting with
+ * the user, but visible to them to some degree); it may also be running
+ * other services under the system's control that it inconsiders important.
+ */
+ public static final int IMPORTANCE_VISIBLE = 200;
+
+ /**
+ * Constant for {@link #importance}: {@link #IMPORTANCE_PERCEPTIBLE} had this wrong value
+ * before {@link Build.VERSION_CODES#O}. Since the {@link Build.VERSION_CODES#O} SDK,
+ * the value of {@link #IMPORTANCE_PERCEPTIBLE} has been fixed.
+ *
+ * <p>The system will return this value instead of {@link #IMPORTANCE_PERCEPTIBLE}
+ * on Android versions below {@link Build.VERSION_CODES#O}.
+ *
+ * <p>On Android version {@link Build.VERSION_CODES#O} and later, this value will still be
+ * returned for apps with the target API level below {@link Build.VERSION_CODES#O}.
+ * For apps targeting version {@link Build.VERSION_CODES#O} and later,
+ * the correct value {@link #IMPORTANCE_PERCEPTIBLE} will be returned.
+ */
+ public static final int IMPORTANCE_PERCEPTIBLE_PRE_26 = 130;
+
+ /**
+ * Constant for {@link #importance}: This process is not something the user
+ * is directly aware of, but is otherwise perceptible to them to some degree.
+ */
+ public static final int IMPORTANCE_PERCEPTIBLE = 230;
+
+ /**
+ * Constant for {@link #importance}: {@link #IMPORTANCE_CANT_SAVE_STATE} had
+ * this wrong value
+ * before {@link Build.VERSION_CODES#O}. Since the {@link Build.VERSION_CODES#O} SDK,
+ * the value of {@link #IMPORTANCE_CANT_SAVE_STATE} has been fixed.
+ *
+ * <p>The system will return this value instead of {@link #IMPORTANCE_CANT_SAVE_STATE}
+ * on Android versions below {@link Build.VERSION_CODES#O}.
+ *
+ * <p>On Android version {@link Build.VERSION_CODES#O} after, this value will still be
+ * returned for apps with the target API level below {@link Build.VERSION_CODES#O}.
+ * For apps targeting version {@link Build.VERSION_CODES#O} and later,
+ * the correct value {@link #IMPORTANCE_CANT_SAVE_STATE} will be returned.
+ *
+ * @hide
+ */
+ public static final int IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170;
+
+ /**
+ * Constant for {@link #importance}: This process is running an
+ * application that can not save its state, and thus can't be killed
+ * while in the background.
+ * @hide
+ */
+ public static final int IMPORTANCE_CANT_SAVE_STATE= 270;
+
+ /**
+ * Constant for {@link #importance}: This process is contains services
+ * that should remain running. These are background services apps have
+ * started, not something the user is aware of, so they may be killed by
+ * the system relatively freely (though it is generally desired that they
+ * stay running as long as they want to).
+ */
+ public static final int IMPORTANCE_SERVICE = 300;
+
+ /**
+ * Constant for {@link #importance}: This process process contains
+ * cached code that is expendable, not actively running any app components
+ * we care about.
+ */
+ public static final int IMPORTANCE_CACHED = 400;
+
+ /**
+ * @deprecated Renamed to {@link #IMPORTANCE_CACHED}.
+ */
+ public static final int IMPORTANCE_BACKGROUND = IMPORTANCE_CACHED;
+
+ /**
+ * Constant for {@link #importance}: This process is empty of any
+ * actively running code.
+ * @deprecated This value is no longer reported, use {@link #IMPORTANCE_CACHED} instead.
+ */
+ @Deprecated
+ public static final int IMPORTANCE_EMPTY = 500;
+
+ /**
+ * Constant for {@link #importance}: This process does not exist.
+ */
+ public static final int IMPORTANCE_GONE = 1000;
+
+ /**
+ * Convert a proc state to the correspondent IMPORTANCE_* constant. If the return value
+ * will be passed to a client, use {@link #procStateToImportanceForClient}.
+ * @hide
+ */
+ public static @Importance int procStateToImportance(int procState) {
+ if (procState == PROCESS_STATE_NONEXISTENT) {
+ return IMPORTANCE_GONE;
+ } else if (procState >= PROCESS_STATE_HOME) {
+ return IMPORTANCE_CACHED;
+ } else if (procState >= PROCESS_STATE_SERVICE) {
+ return IMPORTANCE_SERVICE;
+ } else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
+ return IMPORTANCE_CANT_SAVE_STATE;
+ } else if (procState >= PROCESS_STATE_TRANSIENT_BACKGROUND) {
+ return IMPORTANCE_PERCEPTIBLE;
+ } else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
+ return IMPORTANCE_VISIBLE;
+ } else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
+ return IMPORTANCE_TOP_SLEEPING;
+ } else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
+ return IMPORTANCE_FOREGROUND_SERVICE;
+ } else {
+ return IMPORTANCE_FOREGROUND;
+ }
+ }
+
+ /**
+ * Convert a proc state to the correspondent IMPORTANCE_* constant for a client represented
+ * by a given {@link Context}, with converting {@link #IMPORTANCE_PERCEPTIBLE}
+ * and {@link #IMPORTANCE_CANT_SAVE_STATE} to the corresponding "wrong" value if the
+ * client's target SDK < {@link VERSION_CODES#O}.
+ * @hide
+ */
+ public static @Importance int procStateToImportanceForClient(int procState,
+ Context clientContext) {
+ return procStateToImportanceForTargetSdk(procState,
+ clientContext.getApplicationInfo().targetSdkVersion);
+ }
+
+ /**
+ * See {@link #procStateToImportanceForClient}.
+ * @hide
+ */
+ public static @Importance int procStateToImportanceForTargetSdk(int procState,
+ int targetSdkVersion) {
+ final int importance = procStateToImportance(procState);
+
+ // For pre O apps, convert to the old, wrong values.
+ if (targetSdkVersion < VERSION_CODES.O) {
+ switch (importance) {
+ case IMPORTANCE_PERCEPTIBLE:
+ return IMPORTANCE_PERCEPTIBLE_PRE_26;
+ case IMPORTANCE_CANT_SAVE_STATE:
+ return IMPORTANCE_CANT_SAVE_STATE_PRE_26;
+ }
+ }
+ return importance;
+ }
+
+ /** @hide */
+ public static int importanceToProcState(@Importance int importance) {
+ if (importance == IMPORTANCE_GONE) {
+ return PROCESS_STATE_NONEXISTENT;
+ } else if (importance >= IMPORTANCE_CACHED) {
+ return PROCESS_STATE_HOME;
+ } else if (importance >= IMPORTANCE_SERVICE) {
+ return PROCESS_STATE_SERVICE;
+ } else if (importance > IMPORTANCE_CANT_SAVE_STATE) {
+ return PROCESS_STATE_HEAVY_WEIGHT;
+ } else if (importance >= IMPORTANCE_PERCEPTIBLE) {
+ return PROCESS_STATE_TRANSIENT_BACKGROUND;
+ } else if (importance >= IMPORTANCE_VISIBLE) {
+ return PROCESS_STATE_IMPORTANT_FOREGROUND;
+ } else if (importance >= IMPORTANCE_TOP_SLEEPING) {
+ return PROCESS_STATE_TOP_SLEEPING;
+ } else if (importance >= IMPORTANCE_FOREGROUND_SERVICE) {
+ return PROCESS_STATE_FOREGROUND_SERVICE;
+ } else {
+ return PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ }
+ }
+
+ /**
+ * The relative importance level that the system places on this process.
+ * These constants are numbered so that "more important" values are
+ * always smaller than "less important" values.
+ */
+ public @Importance int importance;
+
+ /**
+ * An additional ordering within a particular {@link #importance}
+ * category, providing finer-grained information about the relative
+ * utility of processes within a category. This number means nothing
+ * except that a smaller values are more recently used (and thus
+ * more important). Currently an LRU value is only maintained for
+ * the {@link #IMPORTANCE_CACHED} category, though others may
+ * be maintained in the future.
+ */
+ public int lru;
+
+ /**
+ * Constant for {@link #importanceReasonCode}: nothing special has
+ * been specified for the reason for this level.
+ */
+ public static final int REASON_UNKNOWN = 0;
+
+ /**
+ * Constant for {@link #importanceReasonCode}: one of the application's
+ * content providers is being used by another process. The pid of
+ * the client process is in {@link #importanceReasonPid} and the
+ * target provider in this process is in
+ * {@link #importanceReasonComponent}.
+ */
+ public static final int REASON_PROVIDER_IN_USE = 1;
+
+ /**
+ * Constant for {@link #importanceReasonCode}: one of the application's
+ * content providers is being used by another process. The pid of
+ * the client process is in {@link #importanceReasonPid} and the
+ * target provider in this process is in
+ * {@link #importanceReasonComponent}.
+ */
+ public static final int REASON_SERVICE_IN_USE = 2;
+
+ /**
+ * The reason for {@link #importance}, if any.
+ */
+ public int importanceReasonCode;
+
+ /**
+ * For the specified values of {@link #importanceReasonCode}, this
+ * is the process ID of the other process that is a client of this
+ * process. This will be 0 if no other process is using this one.
+ */
+ public int importanceReasonPid;
+
+ /**
+ * For the specified values of {@link #importanceReasonCode}, this
+ * is the name of the component that is being used in this process.
+ */
+ public ComponentName importanceReasonComponent;
+
+ /**
+ * When {@link #importanceReasonPid} is non-0, this is the importance
+ * of the other pid. @hide
+ */
+ public int importanceReasonImportance;
+
+ /**
+ * Current process state, as per PROCESS_STATE_* constants.
+ * @hide
+ */
+ public int processState;
+
+ public RunningAppProcessInfo() {
+ importance = IMPORTANCE_FOREGROUND;
+ importanceReasonCode = REASON_UNKNOWN;
+ processState = PROCESS_STATE_IMPORTANT_FOREGROUND;
+ }
+
+ public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) {
+ processName = pProcessName;
+ pid = pPid;
+ pkgList = pArr;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(processName);
+ dest.writeInt(pid);
+ dest.writeInt(uid);
+ dest.writeStringArray(pkgList);
+ dest.writeInt(this.flags);
+ dest.writeInt(lastTrimLevel);
+ dest.writeInt(importance);
+ dest.writeInt(lru);
+ dest.writeInt(importanceReasonCode);
+ dest.writeInt(importanceReasonPid);
+ ComponentName.writeToParcel(importanceReasonComponent, dest);
+ dest.writeInt(importanceReasonImportance);
+ dest.writeInt(processState);
+ }
+
+ public void readFromParcel(Parcel source) {
+ processName = source.readString();
+ pid = source.readInt();
+ uid = source.readInt();
+ pkgList = source.readStringArray();
+ flags = source.readInt();
+ lastTrimLevel = source.readInt();
+ importance = source.readInt();
+ lru = source.readInt();
+ importanceReasonCode = source.readInt();
+ importanceReasonPid = source.readInt();
+ importanceReasonComponent = ComponentName.readFromParcel(source);
+ importanceReasonImportance = source.readInt();
+ processState = source.readInt();
+ }
+
+ public static final Creator<RunningAppProcessInfo> CREATOR =
+ new Creator<RunningAppProcessInfo>() {
+ public RunningAppProcessInfo createFromParcel(Parcel source) {
+ return new RunningAppProcessInfo(source);
+ }
+ public RunningAppProcessInfo[] newArray(int size) {
+ return new RunningAppProcessInfo[size];
+ }
+ };
+
+ private RunningAppProcessInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Returns a list of application processes installed on external media
+ * that are running on the device.
+ *
+ * <p><b>Note: this method is only intended for debugging or building
+ * a user-facing process management UI.</b></p>
+ *
+ * @return Returns a list of ApplicationInfo records, or null if none
+ * This list ordering is not specified.
+ * @hide
+ */
+ public List<ApplicationInfo> getRunningExternalApplications() {
+ try {
+ return getService().getRunningExternalApplications();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the memory trim mode for a process and schedules a memory trim operation.
+ *
+ * <p><b>Note: this method is only intended for testing framework.</b></p>
+ *
+ * @return Returns true if successful.
+ * @hide
+ */
+ public boolean setProcessMemoryTrimLevel(String process, int userId, int level) {
+ try {
+ return getService().setProcessMemoryTrimLevel(process, userId,
+ level);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns a list of application processes that are running on the device.
+ *
+ * <p><b>Note: this method is only intended for debugging or building
+ * a user-facing process management UI.</b></p>
+ *
+ * @return Returns a list of RunningAppProcessInfo records, or null if there are no
+ * running processes (it will not return an empty list). This list ordering is not
+ * specified.
+ */
+ public List<RunningAppProcessInfo> getRunningAppProcesses() {
+ try {
+ return getService().getRunningAppProcesses();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the importance of a given package name, based on the processes that are
+ * currently running. The return value is one of the importance constants defined
+ * in {@link RunningAppProcessInfo}, giving you the highest importance of all the
+ * processes that this package has code running inside of. If there are no processes
+ * running its code, {@link RunningAppProcessInfo#IMPORTANCE_GONE} is returned.
+ * @hide
+ */
+ @SystemApi @TestApi
+ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
+ public @RunningAppProcessInfo.Importance int getPackageImportance(String packageName) {
+ try {
+ int procState = getService().getPackageProcessState(packageName,
+ mContext.getOpPackageName());
+ return RunningAppProcessInfo.procStateToImportanceForClient(procState, mContext);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the importance of a given uid, based on the processes that are
+ * currently running. The return value is one of the importance constants defined
+ * in {@link RunningAppProcessInfo}, giving you the highest importance of all the
+ * processes that this uid has running. If there are no processes
+ * running its code, {@link RunningAppProcessInfo#IMPORTANCE_GONE} is returned.
+ * @hide
+ */
+ @SystemApi @TestApi
+ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
+ public @RunningAppProcessInfo.Importance int getUidImportance(int uid) {
+ try {
+ int procState = getService().getUidProcessState(uid,
+ mContext.getOpPackageName());
+ return RunningAppProcessInfo.procStateToImportanceForClient(procState, mContext);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Callback to get reports about changes to the importance of a uid. Use with
+ * {@link #addOnUidImportanceListener}.
+ * @hide
+ */
+ @SystemApi @TestApi
+ public interface OnUidImportanceListener {
+ /**
+ * The importance if a given uid has changed. Will be one of the importance
+ * values in {@link RunningAppProcessInfo};
+ * {@link RunningAppProcessInfo#IMPORTANCE_GONE IMPORTANCE_GONE} will be reported
+ * when the uid is no longer running at all. This callback will happen on a thread
+ * from a thread pool, not the main UI thread.
+ * @param uid The uid whose importance has changed.
+ * @param importance The new importance value as per {@link RunningAppProcessInfo}.
+ */
+ void onUidImportance(int uid, @RunningAppProcessInfo.Importance int importance);
+ }
+
+ /**
+ * Start monitoring changes to the imoportance of uids running in the system.
+ * @param listener The listener callback that will receive change reports.
+ * @param importanceCutpoint The level of importance in which the caller is interested
+ * in differences. For example, if {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}
+ * is used here, you will receive a call each time a uids importance transitions between
+ * being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and
+ * > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}.
+ *
+ * <p>The caller must hold the {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
+ * permission to use this feature.</p>
+ *
+ * @throws IllegalArgumentException If the listener is already registered.
+ * @throws SecurityException If the caller does not hold
+ * {@link android.Manifest.permission#PACKAGE_USAGE_STATS}.
+ * @hide
+ */
+ @SystemApi @TestApi
+ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
+ public void addOnUidImportanceListener(OnUidImportanceListener listener,
+ @RunningAppProcessInfo.Importance int importanceCutpoint) {
+ synchronized (this) {
+ if (mImportanceListeners.containsKey(listener)) {
+ throw new IllegalArgumentException("Listener already registered: " + listener);
+ }
+ // TODO: implement the cut point in the system process to avoid IPCs.
+ UidObserver observer = new UidObserver(listener, mContext);
+ try {
+ getService().registerUidObserver(observer,
+ UID_OBSERVER_PROCSTATE | UID_OBSERVER_GONE,
+ RunningAppProcessInfo.importanceToProcState(importanceCutpoint),
+ mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mImportanceListeners.put(listener, observer);
+ }
+ }
+
+ /**
+ * Remove an importance listener that was previously registered with
+ * {@link #addOnUidImportanceListener}.
+ *
+ * @throws IllegalArgumentException If the listener is not registered.
+ * @hide
+ */
+ @SystemApi @TestApi
+ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
+ public void removeOnUidImportanceListener(OnUidImportanceListener listener) {
+ synchronized (this) {
+ UidObserver observer = mImportanceListeners.remove(listener);
+ if (observer == null) {
+ throw new IllegalArgumentException("Listener not registered: " + listener);
+ }
+ try {
+ getService().unregisterUidObserver(observer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Return global memory state information for the calling process. This
+ * does not fill in all fields of the {@link RunningAppProcessInfo}. The
+ * only fields that will be filled in are
+ * {@link RunningAppProcessInfo#pid},
+ * {@link RunningAppProcessInfo#uid},
+ * {@link RunningAppProcessInfo#lastTrimLevel},
+ * {@link RunningAppProcessInfo#importance},
+ * {@link RunningAppProcessInfo#lru}, and
+ * {@link RunningAppProcessInfo#importanceReasonCode}.
+ */
+ static public void getMyMemoryState(RunningAppProcessInfo outState) {
+ try {
+ getService().getMyMemoryState(outState);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return information about the memory usage of one or more processes.
+ *
+ * <p><b>Note: this method is only intended for debugging or building
+ * a user-facing process management UI.</b></p>
+ *
+ * @param pids The pids of the processes whose memory usage is to be
+ * retrieved.
+ * @return Returns an array of memory information, one for each
+ * requested pid.
+ */
+ public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) {
+ try {
+ return getService().getProcessMemoryInfo(pids);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @deprecated This is now just a wrapper for
+ * {@link #killBackgroundProcesses(String)}; the previous behavior here
+ * is no longer available to applications because it allows them to
+ * break other applications by removing their alarms, stopping their
+ * services, etc.
+ */
+ @Deprecated
+ public void restartPackage(String packageName) {
+ killBackgroundProcesses(packageName);
+ }
+
+ /**
+ * Have the system immediately kill all background processes associated
+ * with the given package. This is the same as the kernel killing those
+ * processes to reclaim memory; the system will take care of restarting
+ * these processes in the future as needed.
+ *
+ * @param packageName The name of the package whose processes are to
+ * be killed.
+ */
+ @RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
+ public void killBackgroundProcesses(String packageName) {
+ try {
+ getService().killBackgroundProcesses(packageName,
+ UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Kills the specified UID.
+ * @param uid The UID to kill.
+ * @param reason The reason for the kill.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.KILL_UID)
+ public void killUid(int uid, String reason) {
+ try {
+ getService().killUid(UserHandle.getAppId(uid),
+ UserHandle.getUserId(uid), reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Have the system perform a force stop of everything associated with
+ * the given application package. All processes that share its uid
+ * will be killed, all services it has running stopped, all activities
+ * removed, etc. In addition, a {@link Intent#ACTION_PACKAGE_RESTARTED}
+ * broadcast will be sent, so that any of its registered alarms can
+ * be stopped, notifications removed, etc.
+ *
+ * <p>You must hold the permission
+ * {@link android.Manifest.permission#FORCE_STOP_PACKAGES} to be able to
+ * call this method.
+ *
+ * @param packageName The name of the package to be stopped.
+ * @param userId The user for which the running package is to be stopped.
+ *
+ * @hide This is not available to third party applications due to
+ * it allowing them to break other applications by stopping their
+ * services, removing their alarms, etc.
+ */
+ public void forceStopPackageAsUser(String packageName, int userId) {
+ try {
+ getService().forceStopPackage(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @see #forceStopPackageAsUser(String, int)
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.FORCE_STOP_PACKAGES)
+ public void forceStopPackage(String packageName) {
+ forceStopPackageAsUser(packageName, UserHandle.myUserId());
+ }
+
+ /**
+ * Get the device configuration attributes.
+ */
+ public ConfigurationInfo getDeviceConfigurationInfo() {
+ try {
+ return getService().getDeviceConfigurationInfo();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the preferred density of icons for the launcher. This is used when
+ * custom drawables are created (e.g., for shortcuts).
+ *
+ * @return density in terms of DPI
+ */
+ public int getLauncherLargeIconDensity() {
+ final Resources res = mContext.getResources();
+ final int density = res.getDisplayMetrics().densityDpi;
+ final int sw = res.getConfiguration().smallestScreenWidthDp;
+
+ if (sw < 600) {
+ // Smaller than approx 7" tablets, use the regular icon size.
+ return density;
+ }
+
+ switch (density) {
+ case DisplayMetrics.DENSITY_LOW:
+ return DisplayMetrics.DENSITY_MEDIUM;
+ case DisplayMetrics.DENSITY_MEDIUM:
+ return DisplayMetrics.DENSITY_HIGH;
+ case DisplayMetrics.DENSITY_TV:
+ return DisplayMetrics.DENSITY_XHIGH;
+ case DisplayMetrics.DENSITY_HIGH:
+ return DisplayMetrics.DENSITY_XHIGH;
+ case DisplayMetrics.DENSITY_XHIGH:
+ return DisplayMetrics.DENSITY_XXHIGH;
+ case DisplayMetrics.DENSITY_XXHIGH:
+ return DisplayMetrics.DENSITY_XHIGH * 2;
+ default:
+ // The density is some abnormal value. Return some other
+ // abnormal value that is a reasonable scaling of it.
+ return (int)((density*1.5f)+.5f);
+ }
+ }
+
+ /**
+ * Get the preferred launcher icon size. This is used when custom drawables
+ * are created (e.g., for shortcuts).
+ *
+ * @return dimensions of square icons in terms of pixels
+ */
+ public int getLauncherLargeIconSize() {
+ return getLauncherLargeIconSizeInner(mContext);
+ }
+
+ static int getLauncherLargeIconSizeInner(Context context) {
+ final Resources res = context.getResources();
+ final int size = res.getDimensionPixelSize(android.R.dimen.app_icon_size);
+ final int sw = res.getConfiguration().smallestScreenWidthDp;
+
+ if (sw < 600) {
+ // Smaller than approx 7" tablets, use the regular icon size.
+ return size;
+ }
+
+ final int density = res.getDisplayMetrics().densityDpi;
+
+ switch (density) {
+ case DisplayMetrics.DENSITY_LOW:
+ return (size * DisplayMetrics.DENSITY_MEDIUM) / DisplayMetrics.DENSITY_LOW;
+ case DisplayMetrics.DENSITY_MEDIUM:
+ return (size * DisplayMetrics.DENSITY_HIGH) / DisplayMetrics.DENSITY_MEDIUM;
+ case DisplayMetrics.DENSITY_TV:
+ return (size * DisplayMetrics.DENSITY_XHIGH) / DisplayMetrics.DENSITY_HIGH;
+ case DisplayMetrics.DENSITY_HIGH:
+ return (size * DisplayMetrics.DENSITY_XHIGH) / DisplayMetrics.DENSITY_HIGH;
+ case DisplayMetrics.DENSITY_XHIGH:
+ return (size * DisplayMetrics.DENSITY_XXHIGH) / DisplayMetrics.DENSITY_XHIGH;
+ case DisplayMetrics.DENSITY_XXHIGH:
+ return (size * DisplayMetrics.DENSITY_XHIGH*2) / DisplayMetrics.DENSITY_XXHIGH;
+ default:
+ // The density is some abnormal value. Return some other
+ // abnormal value that is a reasonable scaling of it.
+ return (int)((size*1.5f) + .5f);
+ }
+ }
+
+ /**
+ * Returns "true" if the user interface is currently being messed with
+ * by a monkey.
+ */
+ public static boolean isUserAMonkey() {
+ try {
+ return getService().isUserAMonkey();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns "true" if device is running in a test harness.
+ */
+ public static boolean isRunningInTestHarness() {
+ return SystemProperties.getBoolean("ro.test_harness", false);
+ }
+
+ /**
+ * Returns the launch count of each installed package.
+ *
+ * @hide
+ */
+ /*public Map<String, Integer> getAllPackageLaunchCounts() {
+ try {
+ IUsageStats usageStatsService = IUsageStats.Stub.asInterface(
+ ServiceManager.getService("usagestats"));
+ if (usageStatsService == null) {
+ return new HashMap<String, Integer>();
+ }
+
+ UsageStats.PackageStats[] allPkgUsageStats = usageStatsService.getAllPkgUsageStats(
+ ActivityThread.currentPackageName());
+ if (allPkgUsageStats == null) {
+ return new HashMap<String, Integer>();
+ }
+
+ Map<String, Integer> launchCounts = new HashMap<String, Integer>();
+ for (UsageStats.PackageStats pkgUsageStats : allPkgUsageStats) {
+ launchCounts.put(pkgUsageStats.getPackageName(), pkgUsageStats.getLaunchCount());
+ }
+
+ return launchCounts;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Could not query launch counts", e);
+ return new HashMap<String, Integer>();
+ }
+ }*/
+
+ /** @hide */
+ public static int checkComponentPermission(String permission, int uid,
+ int owningUid, boolean exported) {
+ // Root, system server get to do everything.
+ final int appId = UserHandle.getAppId(uid);
+ if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ // Isolated processes don't get any permissions.
+ if (UserHandle.isIsolated(uid)) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ // If there is a uid that owns whatever is being accessed, it has
+ // blanket access to it regardless of the permissions it requires.
+ if (owningUid >= 0 && UserHandle.isSameApp(uid, owningUid)) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ // If the target is not exported, then nobody else can get to it.
+ if (!exported) {
+ /*
+ RuntimeException here = new RuntimeException("here");
+ here.fillInStackTrace();
+ Slog.w(TAG, "Permission denied: checkComponentPermission() owningUid=" + owningUid,
+ here);
+ */
+ return PackageManager.PERMISSION_DENIED;
+ }
+ if (permission == null) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ try {
+ return AppGlobals.getPackageManager()
+ .checkUidPermission(permission, uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public static int checkUidPermission(String permission, int uid) {
+ try {
+ return AppGlobals.getPackageManager()
+ .checkUidPermission(permission, uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Helper for dealing with incoming user arguments to system service calls.
+ * Takes care of checking permissions and converting USER_CURRENT to the
+ * actual current user.
+ *
+ * @param callingPid The pid of the incoming call, as per Binder.getCallingPid().
+ * @param callingUid The uid of the incoming call, as per Binder.getCallingUid().
+ * @param userId The user id argument supplied by the caller -- this is the user
+ * they want to run as.
+ * @param allowAll If true, we will allow USER_ALL. This means you must be prepared
+ * to get a USER_ALL returned and deal with it correctly. If false,
+ * an exception will be thrown if USER_ALL is supplied.
+ * @param requireFull If true, the caller must hold
+ * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} to be able to run as a
+ * different user than their current process; otherwise they must hold
+ * {@link android.Manifest.permission#INTERACT_ACROSS_USERS}.
+ * @param name Optional textual name of the incoming call; only for generating error messages.
+ * @param callerPackage Optional package name of caller; only for error messages.
+ *
+ * @return Returns the user ID that the call should run as. Will always be a concrete
+ * user number, unless <var>allowAll</var> is true in which case it could also be
+ * USER_ALL.
+ */
+ public static int handleIncomingUser(int callingPid, int callingUid, int userId,
+ boolean allowAll, boolean requireFull, String name, String callerPackage) {
+ if (UserHandle.getUserId(callingUid) == userId) {
+ return userId;
+ }
+ try {
+ return getService().handleIncomingUser(callingPid,
+ callingUid, userId, allowAll, requireFull, name, callerPackage);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the userId of the current foreground user. Requires system permissions.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(anyOf = {
+ "android.permission.INTERACT_ACROSS_USERS",
+ "android.permission.INTERACT_ACROSS_USERS_FULL"
+ })
+ public static int getCurrentUser() {
+ UserInfo ui;
+ try {
+ ui = getService().getCurrentUser();
+ return ui != null ? ui.id : 0;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @param userid the user's id. Zero indicates the default user.
+ * @hide
+ */
+ public boolean switchUser(int userid) {
+ try {
+ return getService().switchUser(userid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Logs out current current foreground user by switching to the system user and stopping the
+ * user being switched from.
+ * @hide
+ */
+ public static void logoutCurrentUser() {
+ int currentUser = ActivityManager.getCurrentUser();
+ if (currentUser != UserHandle.USER_SYSTEM) {
+ try {
+ getService().switchUser(UserHandle.USER_SYSTEM);
+ getService().stopUser(currentUser, /* force= */ false, null);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /** {@hide} */
+ public static final int FLAG_OR_STOPPED = 1 << 0;
+ /** {@hide} */
+ public static final int FLAG_AND_LOCKED = 1 << 1;
+ /** {@hide} */
+ public static final int FLAG_AND_UNLOCKED = 1 << 2;
+ /** {@hide} */
+ public static final int FLAG_AND_UNLOCKING_OR_UNLOCKED = 1 << 3;
+
+ /**
+ * Return whether the given user is actively running. This means that
+ * the user is in the "started" state, not "stopped" -- it is currently
+ * allowed to run code through scheduled alarms, receiving broadcasts,
+ * etc. A started user may be either the current foreground user or a
+ * background user; the result here does not distinguish between the two.
+ * @param userId the user's id. Zero indicates the default user.
+ * @hide
+ */
+ public boolean isUserRunning(int userId) {
+ try {
+ return getService().isUserRunning(userId, 0);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ public boolean isVrModePackageEnabled(ComponentName component) {
+ try {
+ return getService().isVrModePackageEnabled(component);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Perform a system dump of various state associated with the given application
+ * package name. This call blocks while the dump is being performed, so should
+ * not be done on a UI thread. The data will be written to the given file
+ * descriptor as text.
+ * @param fd The file descriptor that the dump should be written to. The file
+ * descriptor is <em>not</em> closed by this function; the caller continues to
+ * own it.
+ * @param packageName The name of the package that is to be dumped.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public void dumpPackageState(FileDescriptor fd, String packageName) {
+ dumpPackageStateStatic(fd, packageName);
+ }
+
+ /**
+ * @hide
+ */
+ public static void dumpPackageStateStatic(FileDescriptor fd, String packageName) {
+ FileOutputStream fout = new FileOutputStream(fd);
+ PrintWriter pw = new FastPrintWriter(fout);
+ dumpService(pw, fd, "package", new String[] { packageName });
+ pw.println();
+ dumpService(pw, fd, Context.ACTIVITY_SERVICE, new String[] {
+ "-a", "package", packageName });
+ pw.println();
+ dumpService(pw, fd, "meminfo", new String[] { "--local", "--package", packageName });
+ pw.println();
+ dumpService(pw, fd, ProcessStats.SERVICE_NAME, new String[] { packageName });
+ pw.println();
+ dumpService(pw, fd, "usagestats", new String[] { "--packages", packageName });
+ pw.println();
+ dumpService(pw, fd, BatteryStats.SERVICE_NAME, new String[] { packageName });
+ pw.flush();
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean isSystemReady() {
+ if (!sSystemReady) {
+ if (ActivityThread.isSystem()) {
+ sSystemReady =
+ LocalServices.getService(ActivityManagerInternal.class).isSystemReady();
+ } else {
+ // Since this is being called from outside system server, system should be
+ // ready by now.
+ sSystemReady = true;
+ }
+ }
+ return sSystemReady;
+ }
+
+ /**
+ * @hide
+ */
+ public static void broadcastStickyIntent(Intent intent, int userId) {
+ broadcastStickyIntent(intent, AppOpsManager.OP_NONE, userId);
+ }
+
+ /**
+ * Convenience for sending a sticky broadcast. For internal use only.
+ *
+ * @hide
+ */
+ public static void broadcastStickyIntent(Intent intent, int appOp, int userId) {
+ try {
+ getService().broadcastIntent(
+ null, intent, null, null, Activity.RESULT_OK, null, null,
+ null /*permission*/, appOp, null, false, true, userId);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
+ String tag) {
+ try {
+ getService().noteWakeupAlarm((ps != null) ? ps.getTarget() : null,
+ sourceUid, sourcePkg, tag);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
+ try {
+ getService().noteAlarmStart((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
+ try {
+ getService().noteAlarmFinish((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static IActivityManager getService() {
+ return IActivityManagerSingleton.get();
+ }
+
+ private static final Singleton<IActivityManager> IActivityManagerSingleton =
+ new Singleton<IActivityManager>() {
+ @Override
+ protected IActivityManager create() {
+ final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
+ final IActivityManager am = IActivityManager.Stub.asInterface(b);
+ return am;
+ }
+ };
+
+ private static void dumpService(PrintWriter pw, FileDescriptor fd, String name, String[] args) {
+ pw.print("DUMP OF SERVICE "); pw.print(name); pw.println(":");
+ IBinder service = ServiceManager.checkService(name);
+ if (service == null) {
+ pw.println(" (Service not found)");
+ return;
+ }
+ TransferPipe tp = null;
+ try {
+ pw.flush();
+ tp = new TransferPipe();
+ tp.setBufferPrefix(" ");
+ service.dumpAsync(tp.getWriteFd().getFileDescriptor(), args);
+ tp.go(fd, 10000);
+ } catch (Throwable e) {
+ if (tp != null) {
+ tp.kill();
+ }
+ pw.println("Failure dumping service:");
+ e.printStackTrace(pw);
+ }
+ }
+
+ /**
+ * Request that the system start watching for the calling process to exceed a pss
+ * size as given here. Once called, the system will look for any occasions where it
+ * sees the associated process with a larger pss size and, when this happens, automatically
+ * pull a heap dump from it and allow the user to share the data. Note that this request
+ * continues running even if the process is killed and restarted. To remove the watch,
+ * use {@link #clearWatchHeapLimit()}.
+ *
+ * <p>This API only work if the calling process has been marked as
+ * {@link ApplicationInfo#FLAG_DEBUGGABLE} or this is running on a debuggable
+ * (userdebug or eng) build.</p>
+ *
+ * <p>Callers can optionally implement {@link #ACTION_REPORT_HEAP_LIMIT} to directly
+ * handle heap limit reports themselves.</p>
+ *
+ * @param pssSize The size in bytes to set the limit at.
+ */
+ public void setWatchHeapLimit(long pssSize) {
+ try {
+ getService().setDumpHeapDebugLimit(null, 0, pssSize,
+ mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Action an app can implement to handle reports from {@link #setWatchHeapLimit(long)}.
+ * If your package has an activity handling this action, it will be launched with the
+ * heap data provided to it the same way as {@link Intent#ACTION_SEND}. Note that to
+ * match the activty must support this action and a MIME type of "*&#47;*".
+ */
+ public static final String ACTION_REPORT_HEAP_LIMIT = "android.app.action.REPORT_HEAP_LIMIT";
+
+ /**
+ * Clear a heap watch limit previously set by {@link #setWatchHeapLimit(long)}.
+ */
+ public void clearWatchHeapLimit() {
+ try {
+ getService().setDumpHeapDebugLimit(null, 0, 0, null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return whether currently in lock task mode. When in this mode
+ * no new tasks can be created or switched to.
+ *
+ * @see Activity#startLockTask()
+ *
+ * @deprecated Use {@link #getLockTaskModeState} instead.
+ */
+ @Deprecated
+ public boolean isInLockTaskMode() {
+ return getLockTaskModeState() != LOCK_TASK_MODE_NONE;
+ }
+
+ /**
+ * Return the current state of task locking. The three possible outcomes
+ * are {@link #LOCK_TASK_MODE_NONE}, {@link #LOCK_TASK_MODE_LOCKED}
+ * and {@link #LOCK_TASK_MODE_PINNED}.
+ *
+ * @see Activity#startLockTask()
+ */
+ public int getLockTaskModeState() {
+ try {
+ return getService().getLockTaskModeState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Enable more aggressive scheduling for latency-sensitive low-runtime VR threads. Only one
+ * thread can be a VR thread in a process at a time, and that thread may be subject to
+ * restrictions on the amount of time it can run.
+ *
+ * If persistent VR mode is set, whatever thread has been granted aggressive scheduling via this
+ * method will return to normal operation, and calling this method will do nothing while
+ * persistent VR mode is enabled.
+ *
+ * To reset the VR thread for an application, a tid of 0 can be passed.
+ *
+ * @see android.os.Process#myTid()
+ * @param tid tid of the VR thread
+ */
+ public static void setVrThread(int tid) {
+ try {
+ getService().setVrThread(tid);
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Enable more aggressive scheduling for latency-sensitive low-runtime VR threads that persist
+ * beyond a single process. Only one thread can be a
+ * persistent VR thread at a time, and that thread may be subject to restrictions on the amount
+ * of time it can run. Calling this method will disable aggressive scheduling for non-persistent
+ * VR threads set via {@link #setVrThread}. If persistent VR mode is disabled then the
+ * persistent VR thread loses its new scheduling priority; this method must be called again to
+ * set the persistent thread.
+ *
+ * To reset the persistent VR thread, a tid of 0 can be passed.
+ *
+ * @see android.os.Process#myTid()
+ * @param tid tid of the VR thread
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.RESTRICTED_VR_ACCESS)
+ public static void setPersistentVrThread(int tid) {
+ try {
+ getService().setPersistentVrThread(tid);
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+
+ /**
+ * The AppTask allows you to manage your own application's tasks.
+ * See {@link android.app.ActivityManager#getAppTasks()}
+ */
+ public static class AppTask {
+ private IAppTask mAppTaskImpl;
+
+ /** @hide */
+ public AppTask(IAppTask task) {
+ mAppTaskImpl = task;
+ }
+
+ /**
+ * Finishes all activities in this task and removes it from the recent tasks list.
+ */
+ public void finishAndRemoveTask() {
+ try {
+ mAppTaskImpl.finishAndRemoveTask();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the RecentTaskInfo associated with this task.
+ *
+ * @return The RecentTaskInfo for this task, or null if the task no longer exists.
+ */
+ public RecentTaskInfo getTaskInfo() {
+ try {
+ return mAppTaskImpl.getTaskInfo();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Bring this task to the foreground. If it contains activities, they will be
+ * brought to the foreground with it and their instances re-created if needed.
+ * If it doesn't contain activities, the root activity of the task will be
+ * re-launched.
+ */
+ public void moveToFront() {
+ try {
+ mAppTaskImpl.moveToFront();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Start an activity in this task. Brings the task to the foreground. If this task
+ * is not currently active (that is, its id < 0), then a new activity for the given
+ * Intent will be launched as the root of the task and the task brought to the
+ * foreground. Otherwise, if this task is currently active and the Intent does not specify
+ * an activity to launch in a new task, then a new activity for the given Intent will
+ * be launched on top of the task and the task brought to the foreground. If this
+ * task is currently active and the Intent specifies {@link Intent#FLAG_ACTIVITY_NEW_TASK}
+ * or would otherwise be launched in to a new task, then the activity not launched but
+ * this task be brought to the foreground and a new intent delivered to the top
+ * activity if appropriate.
+ *
+ * <p>In other words, you generally want to use an Intent here that does not specify
+ * {@link Intent#FLAG_ACTIVITY_NEW_TASK} or {@link Intent#FLAG_ACTIVITY_NEW_DOCUMENT},
+ * and let the system do the right thing.</p>
+ *
+ * @param intent The Intent describing the new activity to be launched on the task.
+ * @param options Optional launch options.
+ *
+ * @see Activity#startActivity(android.content.Intent, android.os.Bundle)
+ */
+ public void startActivity(Context context, Intent intent, Bundle options) {
+ ActivityThread thread = ActivityThread.currentActivityThread();
+ thread.getInstrumentation().execStartActivityFromAppTask(context,
+ thread.getApplicationThread(), mAppTaskImpl, intent, options);
+ }
+
+ /**
+ * Modify the {@link Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} flag in the root
+ * Intent of this AppTask.
+ *
+ * @param exclude If true, {@link Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} will
+ * be set; otherwise, it will be cleared.
+ */
+ public void setExcludeFromRecents(boolean exclude) {
+ try {
+ mAppTaskImpl.setExcludeFromRecents(exclude);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+}
diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java
new file mode 100644
index 00000000..9d14f616
--- /dev/null
+++ b/android/app/ActivityManagerInternal.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.service.voice.IVoiceInteractionSession;
+import android.util.SparseIntArray;
+
+import com.android.internal.app.IVoiceInteractor;
+
+import java.util.List;
+
+/**
+ * Activity manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class ActivityManagerInternal {
+
+ /**
+ * Type for {@link #notifyAppTransitionStarting}: The transition was started because we drew
+ * the splash screen.
+ */
+ public static final int APP_TRANSITION_SPLASH_SCREEN = 1;
+
+ /**
+ * Type for {@link #notifyAppTransitionStarting}: The transition was started because we all
+ * app windows were drawn
+ */
+ public static final int APP_TRANSITION_WINDOWS_DRAWN = 2;
+
+ /**
+ * Type for {@link #notifyAppTransitionStarting}: The transition was started because of a
+ * timeout.
+ */
+ public static final int APP_TRANSITION_TIMEOUT = 3;
+
+ /**
+ * Type for {@link #notifyAppTransitionStarting}: The transition was started because of a
+ * we drew a task snapshot.
+ */
+ public static final int APP_TRANSITION_SNAPSHOT = 4;
+
+ /**
+ * Grant Uri permissions from one app to another. This method only extends
+ * permission grants if {@code callingUid} has permission to them.
+ */
+ public abstract void grantUriPermissionFromIntent(int callingUid, String targetPkg,
+ Intent intent, int targetUserId);
+
+ /**
+ * Verify that calling app has access to the given provider.
+ */
+ public abstract String checkContentProviderAccess(String authority, int userId);
+
+ // Called by the power manager.
+ public abstract void onWakefulnessChanged(int wakefulness);
+
+ public abstract int startIsolatedProcess(String entryPoint, String[] mainArgs,
+ String processName, String abiOverride, int uid, Runnable crashHandler);
+
+ /**
+ * Acquires a sleep token for the specified display with the specified tag.
+ *
+ * @param tag A string identifying the purpose of the token (eg. "Dream").
+ * @param displayId The display to apply the sleep token to.
+ */
+ public abstract SleepToken acquireSleepToken(@NonNull String tag, int displayId);
+
+ /**
+ * Sleep tokens cause the activity manager to put the top activity to sleep.
+ * They are used by components such as dreams that may hide and block interaction
+ * with underlying activities.
+ */
+ public static abstract class SleepToken {
+
+ /**
+ * Releases the sleep token.
+ */
+ public abstract void release();
+ }
+
+ /**
+ * Returns home activity for the specified user.
+ *
+ * @param userId ID of the user or {@link android.os.UserHandle#USER_ALL}
+ */
+ public abstract ComponentName getHomeActivityForUser(int userId);
+
+ /**
+ * Called when a user has been deleted. This can happen during normal device usage
+ * or just at startup, when partially removed users are purged. Any state persisted by the
+ * ActivityManager should be purged now.
+ *
+ * @param userId The user being cleaned up.
+ */
+ public abstract void onUserRemoved(int userId);
+
+ public abstract void onLocalVoiceInteractionStarted(IBinder callingActivity,
+ IVoiceInteractionSession mSession,
+ IVoiceInteractor mInteractor);
+
+ /**
+ * Callback for window manager to let activity manager know that we are finally starting the
+ * app transition;
+ *
+ * @param reasons A map from stack id to a reason integer why the transition was started,, which
+ * must be one of the APP_TRANSITION_* values.
+ * @param timestamp The time at which the app transition started in
+ * {@link SystemClock#uptimeMillis()} timebase.
+ */
+ public abstract void notifyAppTransitionStarting(SparseIntArray reasons, long timestamp);
+
+ /**
+ * Callback for window manager to let activity manager know that the app transition was
+ * cancelled.
+ */
+ public abstract void notifyAppTransitionCancelled();
+
+ /**
+ * Callback for window manager to let activity manager know that the app transition is finished.
+ */
+ public abstract void notifyAppTransitionFinished();
+
+ /**
+ * Returns the top activity from each of the currently visible stacks. The first entry will be
+ * the focused activity.
+ */
+ public abstract List<IBinder> getTopVisibleActivities();
+
+ /**
+ * Callback for window manager to let activity manager know that docked stack changes its
+ * minimized state.
+ */
+ public abstract void notifyDockedStackMinimizedChanged(boolean minimized);
+
+ /**
+ * Kill foreground apps from the specified user.
+ */
+ public abstract void killForegroundAppsForUser(int userHandle);
+
+ /**
+ * Sets how long a {@link PendingIntent} can be temporarily whitelist to by bypass restrictions
+ * such as Power Save mode.
+ */
+ public abstract void setPendingIntentWhitelistDuration(IIntentSender target,
+ IBinder whitelistToken, long duration);
+
+ /**
+ * Allow DeviceIdleController to tell us about what apps are whitelisted.
+ */
+ public abstract void setDeviceIdleWhitelist(int[] appids);
+
+ /**
+ * Update information about which app IDs are on the temp whitelist.
+ */
+ public abstract void updateDeviceIdleTempWhitelist(int[] appids, int changingAppId,
+ boolean adding);
+
+ /**
+ * Updates and persists the {@link Configuration} for a given user.
+ *
+ * @param values the configuration to update
+ * @param userId the user to update the configuration for
+ */
+ public abstract void updatePersistentConfigurationForUser(@NonNull Configuration values,
+ int userId);
+
+ /**
+ * Start activity {@code intents} as if {@code packageName} on user {@code userId} did it.
+ *
+ * @return error codes used by {@link IActivityManager#startActivity} and its siblings.
+ */
+ public abstract int startActivitiesAsPackage(String packageName,
+ int userId, Intent[] intents, Bundle bOptions);
+
+ /**
+ * Get the procstate for the UID. The return value will be between
+ * {@link ActivityManager#MIN_PROCESS_STATE} and {@link ActivityManager#MAX_PROCESS_STATE}.
+ * Note if the UID doesn't exist, it'll return {@link ActivityManager#PROCESS_STATE_NONEXISTENT}
+ * (-1).
+ */
+ public abstract int getUidProcessState(int uid);
+
+ /**
+ * Called when Keyguard flags might have changed.
+ *
+ * @param callback Callback to run after activity visibilities have been reevaluated. This can
+ * be used from window manager so that when the callback is called, it's
+ * guaranteed that all apps have their visibility updated accordingly.
+ */
+ public abstract void notifyKeyguardFlagsChanged(@Nullable Runnable callback);
+
+ /**
+ * @return {@code true} if system is ready, {@code false} otherwise.
+ */
+ public abstract boolean isSystemReady();
+
+ /**
+ * Called when the trusted state of Keyguard has changed.
+ */
+ public abstract void notifyKeyguardTrustedChanged();
+
+ /**
+ * Sets if the given pid has an overlay UI or not.
+ *
+ * @param pid The pid we are setting overlay UI for.
+ * @param hasOverlayUi True if the process has overlay UI.
+ * @see android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY
+ */
+ public abstract void setHasOverlayUi(int pid, boolean hasOverlayUi);
+
+ /**
+ * Called after the network policy rules are updated by
+ * {@link com.android.server.net.NetworkPolicyManagerService} for a specific {@param uid} and
+ * {@param procStateSeq}.
+ */
+ public abstract void notifyNetworkPolicyRulesUpdated(int uid, long procStateSeq);
+
+ /**
+ * Called after virtual display Id is updated by
+ * {@link com.android.server.vr.Vr2dDisplay} with a specific
+ * {@param vr2dDisplayId}.
+ */
+ public abstract void setVr2dDisplayId(int vr2dDisplayId);
+
+ /**
+ * Saves the current activity manager state and includes the saved state in the next dump of
+ * activity manager.
+ */
+ public abstract void saveANRState(String reason);
+
+ /**
+ * Clears the previously saved activity manager ANR state.
+ */
+ public abstract void clearSavedANRState();
+
+ /**
+ * Set focus on an activity.
+ * @param token The IApplicationToken for the activity
+ */
+ public abstract void setFocusedActivity(IBinder token);
+}
diff --git a/android/app/ActivityManagerNative.java b/android/app/ActivityManagerNative.java
new file mode 100644
index 00000000..c09403c2
--- /dev/null
+++ b/android/app/ActivityManagerNative.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * {@hide}
+ * @deprecated will be removed soon. See individual methods for alternatives.
+ */
+@Deprecated
+public abstract class ActivityManagerNative {
+ /**
+ * Cast a Binder object into an activity manager interface, generating
+ * a proxy if needed.
+ *
+ * @deprecated use IActivityManager.Stub.asInterface instead.
+ */
+ static public IActivityManager asInterface(IBinder obj) {
+ return IActivityManager.Stub.asInterface(obj);
+ }
+
+ /**
+ * Retrieve the system's default/global activity manager.
+ *
+ * @deprecated use ActivityManager.getService instead.
+ */
+ static public IActivityManager getDefault() {
+ return ActivityManager.getService();
+ }
+
+ /**
+ * Convenience for checking whether the system is ready. For internal use only.
+ *
+ * @deprecated use ActivityManagerInternal.isSystemReady instead.
+ */
+ static public boolean isSystemReady() {
+ return ActivityManager.isSystemReady();
+ }
+
+ /**
+ * @deprecated use ActivityManager.broadcastStickyIntent instead.
+ */
+ static public void broadcastStickyIntent(Intent intent, String permission, int userId) {
+ broadcastStickyIntent(intent, permission, AppOpsManager.OP_NONE, userId);
+ }
+
+ /**
+ * Convenience for sending a sticky broadcast. For internal use only.
+ * If you don't care about permission, use null.
+ *
+ * @deprecated use ActivityManager.broadcastStickyIntent instead.
+ */
+ static public void broadcastStickyIntent(Intent intent, String permission, int appOp,
+ int userId) {
+ ActivityManager.broadcastStickyIntent(intent, appOp, userId);
+ }
+
+ /**
+ * @deprecated use ActivityManager.noteWakeupAlarm instead.
+ */
+ static public void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
+ String tag) {
+ ActivityManager.noteWakeupAlarm(ps, sourceUid, sourcePkg, tag);
+ }
+
+ /**
+ * @deprecated use ActivityManager.noteAlarmStart instead.
+ */
+ static public void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
+ ActivityManager.noteAlarmStart(ps, sourceUid, tag);
+ }
+
+ /**
+ * @deprecated use ActivityManager.noteAlarmFinish instead.
+ */
+ static public void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
+ ActivityManager.noteAlarmFinish(ps, sourceUid, tag);
+ }
+}
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
new file mode 100644
index 00000000..0bffc9e6
--- /dev/null
+++ b/android/app/ActivityOptions.java
@@ -0,0 +1,1473 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
+import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.GraphicBuffer;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IRemoteCallback;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.transition.Transition;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.util.Pair;
+import android.util.Slog;
+import android.view.AppTransitionAnimationSpec;
+import android.view.IAppTransitionAnimationSpecsFuture;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import java.util.ArrayList;
+
+/**
+ * Helper class for building an options Bundle that can be used with
+ * {@link android.content.Context#startActivity(android.content.Intent, android.os.Bundle)
+ * Context.startActivity(Intent, Bundle)} and related methods.
+ */
+public class ActivityOptions {
+ private static final String TAG = "ActivityOptions";
+
+ /**
+ * A long in the extras delivered by {@link #requestUsageTimeReport} that contains
+ * the total time (in ms) the user spent in the app flow.
+ */
+ public static final String EXTRA_USAGE_TIME_REPORT = "android.activity.usage_time";
+
+ /**
+ * A Bundle in the extras delivered by {@link #requestUsageTimeReport} that contains
+ * detailed information about the time spent in each package associated with the app;
+ * each key is a package name, whose value is a long containing the time (in ms).
+ */
+ public static final String EXTRA_USAGE_TIME_REPORT_PACKAGES = "android.usage_time_packages";
+
+ /**
+ * The package name that created the options.
+ * @hide
+ */
+ public static final String KEY_PACKAGE_NAME = "android:activity.packageName";
+
+ /**
+ * The bounds (window size) that the activity should be launched in. Set to null explicitly for
+ * full screen. If the key is not found, previous bounds will be preserved.
+ * NOTE: This value is ignored on devices that don't have
+ * {@link android.content.pm.PackageManager#FEATURE_FREEFORM_WINDOW_MANAGEMENT} or
+ * {@link android.content.pm.PackageManager#FEATURE_PICTURE_IN_PICTURE} enabled.
+ * @hide
+ */
+ public static final String KEY_LAUNCH_BOUNDS = "android:activity.launchBounds";
+
+ /**
+ * Type of animation that arguments specify.
+ * @hide
+ */
+ public static final String KEY_ANIM_TYPE = "android:activity.animType";
+
+ /**
+ * Custom enter animation resource ID.
+ * @hide
+ */
+ public static final String KEY_ANIM_ENTER_RES_ID = "android:activity.animEnterRes";
+
+ /**
+ * Custom exit animation resource ID.
+ * @hide
+ */
+ public static final String KEY_ANIM_EXIT_RES_ID = "android:activity.animExitRes";
+
+ /**
+ * Custom in-place animation resource ID.
+ * @hide
+ */
+ public static final String KEY_ANIM_IN_PLACE_RES_ID = "android:activity.animInPlaceRes";
+
+ /**
+ * Bitmap for thumbnail animation.
+ * @hide
+ */
+ public static final String KEY_ANIM_THUMBNAIL = "android:activity.animThumbnail";
+
+ /**
+ * Start X position of thumbnail animation.
+ * @hide
+ */
+ public static final String KEY_ANIM_START_X = "android:activity.animStartX";
+
+ /**
+ * Start Y position of thumbnail animation.
+ * @hide
+ */
+ public static final String KEY_ANIM_START_Y = "android:activity.animStartY";
+
+ /**
+ * Initial width of the animation.
+ * @hide
+ */
+ public static final String KEY_ANIM_WIDTH = "android:activity.animWidth";
+
+ /**
+ * Initial height of the animation.
+ * @hide
+ */
+ public static final String KEY_ANIM_HEIGHT = "android:activity.animHeight";
+
+ /**
+ * Callback for when animation is started.
+ * @hide
+ */
+ public static final String KEY_ANIM_START_LISTENER = "android:activity.animStartListener";
+
+ /**
+ * Callback for when the last frame of the animation is played.
+ * @hide
+ */
+ private static final String KEY_ANIMATION_FINISHED_LISTENER =
+ "android:activity.animationFinishedListener";
+
+ /**
+ * Descriptions of app transition animations to be played during the activity launch.
+ */
+ private static final String KEY_ANIM_SPECS = "android:activity.animSpecs";
+
+ /**
+ * The display id the activity should be launched into.
+ * @see #setLaunchDisplayId(int)
+ * @hide
+ */
+ private static final String KEY_LAUNCH_DISPLAY_ID = "android.activity.launchDisplayId";
+
+ /**
+ * The stack id the activity should be launched into.
+ * @hide
+ */
+ private static final String KEY_LAUNCH_STACK_ID = "android.activity.launchStackId";
+
+ /**
+ * The task id the activity should be launched into.
+ * @hide
+ */
+ private static final String KEY_LAUNCH_TASK_ID = "android.activity.launchTaskId";
+
+ /**
+ * See {@link #setTaskOverlay}.
+ * @hide
+ */
+ private static final String KEY_TASK_OVERLAY = "android.activity.taskOverlay";
+
+ /**
+ * See {@link #setTaskOverlay}.
+ * @hide
+ */
+ private static final String KEY_TASK_OVERLAY_CAN_RESUME =
+ "android.activity.taskOverlayCanResume";
+
+ /**
+ * Where the docked stack should be positioned.
+ * @hide
+ */
+ private static final String KEY_DOCK_CREATE_MODE = "android:activity.dockCreateMode";
+
+ /**
+ * Determines whether to disallow the outgoing activity from entering picture-in-picture as the
+ * result of a new activity being launched.
+ * @hide
+ */
+ private static final String KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING =
+ "android:activity.disallowEnterPictureInPictureWhileLaunching";
+
+ /**
+ * For Activity transitions, the calling Activity's TransitionListener used to
+ * notify the called Activity when the shared element and the exit transitions
+ * complete.
+ */
+ private static final String KEY_TRANSITION_COMPLETE_LISTENER
+ = "android:activity.transitionCompleteListener";
+
+ private static final String KEY_TRANSITION_IS_RETURNING
+ = "android:activity.transitionIsReturning";
+ private static final String KEY_TRANSITION_SHARED_ELEMENTS
+ = "android:activity.sharedElementNames";
+ private static final String KEY_RESULT_DATA = "android:activity.resultData";
+ private static final String KEY_RESULT_CODE = "android:activity.resultCode";
+ private static final String KEY_EXIT_COORDINATOR_INDEX
+ = "android:activity.exitCoordinatorIndex";
+
+ private static final String KEY_USAGE_TIME_REPORT = "android:activity.usageTimeReport";
+ private static final String KEY_ROTATION_ANIMATION_HINT = "android:activity.rotationAnimationHint";
+
+ private static final String KEY_INSTANT_APP_VERIFICATION_BUNDLE
+ = "android:instantapps.installerbundle";
+ private static final String KEY_SPECS_FUTURE = "android:activity.specsFuture";
+
+ /** @hide */
+ public static final int ANIM_NONE = 0;
+ /** @hide */
+ public static final int ANIM_CUSTOM = 1;
+ /** @hide */
+ public static final int ANIM_SCALE_UP = 2;
+ /** @hide */
+ public static final int ANIM_THUMBNAIL_SCALE_UP = 3;
+ /** @hide */
+ public static final int ANIM_THUMBNAIL_SCALE_DOWN = 4;
+ /** @hide */
+ public static final int ANIM_SCENE_TRANSITION = 5;
+ /** @hide */
+ public static final int ANIM_DEFAULT = 6;
+ /** @hide */
+ public static final int ANIM_LAUNCH_TASK_BEHIND = 7;
+ /** @hide */
+ public static final int ANIM_THUMBNAIL_ASPECT_SCALE_UP = 8;
+ /** @hide */
+ public static final int ANIM_THUMBNAIL_ASPECT_SCALE_DOWN = 9;
+ /** @hide */
+ public static final int ANIM_CUSTOM_IN_PLACE = 10;
+ /** @hide */
+ public static final int ANIM_CLIP_REVEAL = 11;
+
+ private String mPackageName;
+ private Rect mLaunchBounds;
+ private int mAnimationType = ANIM_NONE;
+ private int mCustomEnterResId;
+ private int mCustomExitResId;
+ private int mCustomInPlaceResId;
+ private Bitmap mThumbnail;
+ private int mStartX;
+ private int mStartY;
+ private int mWidth;
+ private int mHeight;
+ private IRemoteCallback mAnimationStartedListener;
+ private IRemoteCallback mAnimationFinishedListener;
+ private ResultReceiver mTransitionReceiver;
+ private boolean mIsReturning;
+ private ArrayList<String> mSharedElementNames;
+ private Intent mResultData;
+ private int mResultCode;
+ private int mExitCoordinatorIndex;
+ private PendingIntent mUsageTimeReport;
+ private int mLaunchDisplayId = INVALID_DISPLAY;
+ private int mLaunchStackId = INVALID_STACK_ID;
+ private int mLaunchTaskId = -1;
+ private int mDockCreateMode = DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
+ private boolean mDisallowEnterPictureInPictureWhileLaunching;
+ private boolean mTaskOverlay;
+ private boolean mTaskOverlayCanResume;
+ private AppTransitionAnimationSpec mAnimSpecs[];
+ private int mRotationAnimationHint = -1;
+ private Bundle mAppVerificationBundle;
+ private IAppTransitionAnimationSpecsFuture mSpecsFuture;
+
+ /**
+ * Create an ActivityOptions specifying a custom animation to run when
+ * the activity is displayed.
+ *
+ * @param context Who is defining this. This is the application that the
+ * animation resources will be loaded from.
+ * @param enterResId A resource ID of the animation resource to use for
+ * the incoming activity. Use 0 for no animation.
+ * @param exitResId A resource ID of the animation resource to use for
+ * the outgoing activity. Use 0 for no animation.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptions makeCustomAnimation(Context context,
+ int enterResId, int exitResId) {
+ return makeCustomAnimation(context, enterResId, exitResId, null, null);
+ }
+
+ /**
+ * Create an ActivityOptions specifying a custom animation to run when
+ * the activity is displayed.
+ *
+ * @param context Who is defining this. This is the application that the
+ * animation resources will be loaded from.
+ * @param enterResId A resource ID of the animation resource to use for
+ * the incoming activity. Use 0 for no animation.
+ * @param exitResId A resource ID of the animation resource to use for
+ * the outgoing activity. Use 0 for no animation.
+ * @param handler If <var>listener</var> is non-null this must be a valid
+ * Handler on which to dispatch the callback; otherwise it should be null.
+ * @param listener Optional OnAnimationStartedListener to find out when the
+ * requested animation has started running. If for some reason the animation
+ * is not executed, the callback will happen immediately.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ * @hide
+ */
+ public static ActivityOptions makeCustomAnimation(Context context,
+ int enterResId, int exitResId, Handler handler, OnAnimationStartedListener listener) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = context.getPackageName();
+ opts.mAnimationType = ANIM_CUSTOM;
+ opts.mCustomEnterResId = enterResId;
+ opts.mCustomExitResId = exitResId;
+ opts.setOnAnimationStartedListener(handler, listener);
+ return opts;
+ }
+
+ /**
+ * Creates an ActivityOptions specifying a custom animation to run in place on an existing
+ * activity.
+ *
+ * @param context Who is defining this. This is the application that the
+ * animation resources will be loaded from.
+ * @param animId A resource ID of the animation resource to use for
+ * the incoming activity.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when running an in-place animation.
+ * @hide
+ */
+ public static ActivityOptions makeCustomInPlaceAnimation(Context context, int animId) {
+ if (animId == 0) {
+ throw new RuntimeException("You must specify a valid animation.");
+ }
+
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = context.getPackageName();
+ opts.mAnimationType = ANIM_CUSTOM_IN_PLACE;
+ opts.mCustomInPlaceResId = animId;
+ return opts;
+ }
+
+ private void setOnAnimationStartedListener(final Handler handler,
+ final OnAnimationStartedListener listener) {
+ if (listener != null) {
+ mAnimationStartedListener = new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ handler.post(new Runnable() {
+ @Override public void run() {
+ listener.onAnimationStarted();
+ }
+ });
+ }
+ };
+ }
+ }
+
+ /**
+ * Callback for use with {@link ActivityOptions#makeThumbnailScaleUpAnimation}
+ * to find out when the given animation has started running.
+ * @hide
+ */
+ public interface OnAnimationStartedListener {
+ void onAnimationStarted();
+ }
+
+ private void setOnAnimationFinishedListener(final Handler handler,
+ final OnAnimationFinishedListener listener) {
+ if (listener != null) {
+ mAnimationFinishedListener = new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAnimationFinished();
+ }
+ });
+ }
+ };
+ }
+ }
+
+ /**
+ * Callback for use with {@link ActivityOptions#makeThumbnailAspectScaleDownAnimation}
+ * to find out when the given animation has drawn its last frame.
+ * @hide
+ */
+ public interface OnAnimationFinishedListener {
+ void onAnimationFinished();
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where the new
+ * activity is scaled from a small originating area of the screen to
+ * its final full representation.
+ *
+ * <p>If the Intent this is being used with has not set its
+ * {@link android.content.Intent#setSourceBounds Intent.setSourceBounds},
+ * those bounds will be filled in for you based on the initial
+ * bounds passed in here.
+ *
+ * @param source The View that the new activity is animating from. This
+ * defines the coordinate space for <var>startX</var> and <var>startY</var>.
+ * @param startX The x starting location of the new activity, relative to <var>source</var>.
+ * @param startY The y starting location of the activity, relative to <var>source</var>.
+ * @param width The initial width of the new activity.
+ * @param height The initial height of the new activity.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptions makeScaleUpAnimation(View source,
+ int startX, int startY, int width, int height) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = source.getContext().getPackageName();
+ opts.mAnimationType = ANIM_SCALE_UP;
+ int[] pts = new int[2];
+ source.getLocationOnScreen(pts);
+ opts.mStartX = pts[0] + startX;
+ opts.mStartY = pts[1] + startY;
+ opts.mWidth = width;
+ opts.mHeight = height;
+ return opts;
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where the new
+ * activity is revealed from a small originating area of the screen to
+ * its final full representation.
+ *
+ * @param source The View that the new activity is animating from. This
+ * defines the coordinate space for <var>startX</var> and <var>startY</var>.
+ * @param startX The x starting location of the new activity, relative to <var>source</var>.
+ * @param startY The y starting location of the activity, relative to <var>source</var>.
+ * @param width The initial width of the new activity.
+ * @param height The initial height of the new activity.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptions makeClipRevealAnimation(View source,
+ int startX, int startY, int width, int height) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mAnimationType = ANIM_CLIP_REVEAL;
+ int[] pts = new int[2];
+ source.getLocationOnScreen(pts);
+ opts.mStartX = pts[0] + startX;
+ opts.mStartY = pts[1] + startY;
+ opts.mWidth = width;
+ opts.mHeight = height;
+ return opts;
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where a thumbnail
+ * is scaled from a given position to the new activity window that is
+ * being started.
+ *
+ * <p>If the Intent this is being used with has not set its
+ * {@link android.content.Intent#setSourceBounds Intent.setSourceBounds},
+ * those bounds will be filled in for you based on the initial
+ * thumbnail location and size provided here.
+ *
+ * @param source The View that this thumbnail is animating from. This
+ * defines the coordinate space for <var>startX</var> and <var>startY</var>.
+ * @param thumbnail The bitmap that will be shown as the initial thumbnail
+ * of the animation.
+ * @param startX The x starting location of the bitmap, relative to <var>source</var>.
+ * @param startY The y starting location of the bitmap, relative to <var>source</var>.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptions makeThumbnailScaleUpAnimation(View source,
+ Bitmap thumbnail, int startX, int startY) {
+ return makeThumbnailScaleUpAnimation(source, thumbnail, startX, startY, null);
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where a thumbnail
+ * is scaled from a given position to the new activity window that is
+ * being started.
+ *
+ * @param source The View that this thumbnail is animating from. This
+ * defines the coordinate space for <var>startX</var> and <var>startY</var>.
+ * @param thumbnail The bitmap that will be shown as the initial thumbnail
+ * of the animation.
+ * @param startX The x starting location of the bitmap, relative to <var>source</var>.
+ * @param startY The y starting location of the bitmap, relative to <var>source</var>.
+ * @param listener Optional OnAnimationStartedListener to find out when the
+ * requested animation has started running. If for some reason the animation
+ * is not executed, the callback will happen immediately.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ private static ActivityOptions makeThumbnailScaleUpAnimation(View source,
+ Bitmap thumbnail, int startX, int startY, OnAnimationStartedListener listener) {
+ return makeThumbnailAnimation(source, thumbnail, startX, startY, listener, true);
+ }
+
+ private static ActivityOptions makeThumbnailAnimation(View source,
+ Bitmap thumbnail, int startX, int startY, OnAnimationStartedListener listener,
+ boolean scaleUp) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = source.getContext().getPackageName();
+ opts.mAnimationType = scaleUp ? ANIM_THUMBNAIL_SCALE_UP : ANIM_THUMBNAIL_SCALE_DOWN;
+ opts.mThumbnail = thumbnail;
+ int[] pts = new int[2];
+ source.getLocationOnScreen(pts);
+ opts.mStartX = pts[0] + startX;
+ opts.mStartY = pts[1] + startY;
+ opts.setOnAnimationStartedListener(source.getHandler(), listener);
+ return opts;
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where a list of activity windows and
+ * thumbnails are aspect scaled to/from a new location.
+ * @hide
+ */
+ public static ActivityOptions makeMultiThumbFutureAspectScaleAnimation(Context context,
+ Handler handler, IAppTransitionAnimationSpecsFuture specsFuture,
+ OnAnimationStartedListener listener, boolean scaleUp) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = context.getPackageName();
+ opts.mAnimationType = scaleUp
+ ? ANIM_THUMBNAIL_ASPECT_SCALE_UP
+ : ANIM_THUMBNAIL_ASPECT_SCALE_DOWN;
+ opts.mSpecsFuture = specsFuture;
+ opts.setOnAnimationStartedListener(handler, listener);
+ return opts;
+ }
+
+ /**
+ * Create an ActivityOptions specifying an animation where the new activity
+ * window and a thumbnail is aspect-scaled to a new location.
+ *
+ * @param source The View that this thumbnail is animating to. This
+ * defines the coordinate space for <var>startX</var> and <var>startY</var>.
+ * @param thumbnail The bitmap that will be shown as the final thumbnail
+ * of the animation.
+ * @param startX The x end location of the bitmap, relative to <var>source</var>.
+ * @param startY The y end location of the bitmap, relative to <var>source</var>.
+ * @param handler If <var>listener</var> is non-null this must be a valid
+ * Handler on which to dispatch the callback; otherwise it should be null.
+ * @param listener Optional OnAnimationStartedListener to find out when the
+ * requested animation has started running. If for some reason the animation
+ * is not executed, the callback will happen immediately.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ * @hide
+ */
+ public static ActivityOptions makeThumbnailAspectScaleDownAnimation(View source,
+ Bitmap thumbnail, int startX, int startY, int targetWidth, int targetHeight,
+ Handler handler, OnAnimationStartedListener listener) {
+ return makeAspectScaledThumbnailAnimation(source, thumbnail, startX, startY,
+ targetWidth, targetHeight, handler, listener, false);
+ }
+
+ private static ActivityOptions makeAspectScaledThumbnailAnimation(View source, Bitmap thumbnail,
+ int startX, int startY, int targetWidth, int targetHeight,
+ Handler handler, OnAnimationStartedListener listener, boolean scaleUp) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = source.getContext().getPackageName();
+ opts.mAnimationType = scaleUp ? ANIM_THUMBNAIL_ASPECT_SCALE_UP :
+ ANIM_THUMBNAIL_ASPECT_SCALE_DOWN;
+ opts.mThumbnail = thumbnail;
+ int[] pts = new int[2];
+ source.getLocationOnScreen(pts);
+ opts.mStartX = pts[0] + startX;
+ opts.mStartY = pts[1] + startY;
+ opts.mWidth = targetWidth;
+ opts.mHeight = targetHeight;
+ opts.setOnAnimationStartedListener(handler, listener);
+ return opts;
+ }
+
+ /** @hide */
+ public static ActivityOptions makeThumbnailAspectScaleDownAnimation(View source,
+ AppTransitionAnimationSpec[] specs, Handler handler,
+ OnAnimationStartedListener onAnimationStartedListener,
+ OnAnimationFinishedListener onAnimationFinishedListener) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mPackageName = source.getContext().getPackageName();
+ opts.mAnimationType = ANIM_THUMBNAIL_ASPECT_SCALE_DOWN;
+ opts.mAnimSpecs = specs;
+ opts.setOnAnimationStartedListener(handler, onAnimationStartedListener);
+ opts.setOnAnimationFinishedListener(handler, onAnimationFinishedListener);
+ return opts;
+ }
+
+ /**
+ * Create an ActivityOptions to transition between Activities using cross-Activity scene
+ * animations. This method carries the position of one shared element to the started Activity.
+ * The position of <code>sharedElement</code> will be used as the epicenter for the
+ * exit Transition. The position of the shared element in the launched Activity will be the
+ * epicenter of its entering Transition.
+ *
+ * <p>This requires {@link android.view.Window#FEATURE_ACTIVITY_TRANSITIONS} to be
+ * enabled on the calling Activity to cause an exit transition. The same must be in
+ * the called Activity to get an entering transition.</p>
+ * @param activity The Activity whose window contains the shared elements.
+ * @param sharedElement The View to transition to the started Activity.
+ * @param sharedElementName The shared element name as used in the target Activity. This
+ * must not be null.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ * @see android.transition.Transition#setEpicenterCallback(
+ * android.transition.Transition.EpicenterCallback)
+ */
+ public static ActivityOptions makeSceneTransitionAnimation(Activity activity,
+ View sharedElement, String sharedElementName) {
+ return makeSceneTransitionAnimation(activity, Pair.create(sharedElement, sharedElementName));
+ }
+
+ /**
+ * Create an ActivityOptions to transition between Activities using cross-Activity scene
+ * animations. This method carries the position of multiple shared elements to the started
+ * Activity. The position of the first element in sharedElements
+ * will be used as the epicenter for the exit Transition. The position of the associated
+ * shared element in the launched Activity will be the epicenter of its entering Transition.
+ *
+ * <p>This requires {@link android.view.Window#FEATURE_ACTIVITY_TRANSITIONS} to be
+ * enabled on the calling Activity to cause an exit transition. The same must be in
+ * the called Activity to get an entering transition.</p>
+ * @param activity The Activity whose window contains the shared elements.
+ * @param sharedElements The names of the shared elements to transfer to the called
+ * Activity and their associated Views. The Views must each have
+ * a unique shared element name.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ * @see android.transition.Transition#setEpicenterCallback(
+ * android.transition.Transition.EpicenterCallback)
+ */
+ @SafeVarargs
+ public static ActivityOptions makeSceneTransitionAnimation(Activity activity,
+ Pair<View, String>... sharedElements) {
+ ActivityOptions opts = new ActivityOptions();
+ makeSceneTransitionAnimation(activity, activity.getWindow(), opts,
+ activity.mExitTransitionListener, sharedElements);
+ return opts;
+ }
+
+ /**
+ * Call this immediately prior to startActivity to begin a shared element transition
+ * from a non-Activity. The window must support Window.FEATURE_ACTIVITY_TRANSITIONS.
+ * The exit transition will start immediately and the shared element transition will
+ * start once the launched Activity's shared element is ready.
+ * <p>
+ * When all transitions have completed and the shared element has been transfered,
+ * the window's decor View will have its visibility set to View.GONE.
+ *
+ * @hide
+ */
+ @SafeVarargs
+ public static ActivityOptions startSharedElementAnimation(Window window,
+ Pair<View, String>... sharedElements) {
+ ActivityOptions opts = new ActivityOptions();
+ final View decorView = window.getDecorView();
+ if (decorView == null) {
+ return opts;
+ }
+ final ExitTransitionCoordinator exit =
+ makeSceneTransitionAnimation(null, window, opts, null, sharedElements);
+ if (exit != null) {
+ HideWindowListener listener = new HideWindowListener(window, exit);
+ exit.setHideSharedElementsCallback(listener);
+ exit.startExit();
+ }
+ return opts;
+ }
+
+ /**
+ * This method should be called when the {@link #startSharedElementAnimation(Window, Pair[])}
+ * animation must be stopped and the Views reset. This can happen if there was an error
+ * from startActivity or a springboard activity and the animation should stop and reset.
+ *
+ * @hide
+ */
+ public static void stopSharedElementAnimation(Window window) {
+ final View decorView = window.getDecorView();
+ if (decorView == null) {
+ return;
+ }
+ final ExitTransitionCoordinator exit = (ExitTransitionCoordinator)
+ decorView.getTag(com.android.internal.R.id.cross_task_transition);
+ if (exit != null) {
+ exit.cancelPendingTransitions();
+ decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, null);
+ TransitionManager.endTransitions((ViewGroup) decorView);
+ exit.resetViews();
+ exit.clearState();
+ decorView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ static ExitTransitionCoordinator makeSceneTransitionAnimation(Activity activity, Window window,
+ ActivityOptions opts, SharedElementCallback callback,
+ Pair<View, String>[] sharedElements) {
+ if (!window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
+ opts.mAnimationType = ANIM_DEFAULT;
+ return null;
+ }
+ opts.mAnimationType = ANIM_SCENE_TRANSITION;
+
+ ArrayList<String> names = new ArrayList<String>();
+ ArrayList<View> views = new ArrayList<View>();
+
+ if (sharedElements != null) {
+ for (int i = 0; i < sharedElements.length; i++) {
+ Pair<View, String> sharedElement = sharedElements[i];
+ String sharedElementName = sharedElement.second;
+ if (sharedElementName == null) {
+ throw new IllegalArgumentException("Shared element name must not be null");
+ }
+ names.add(sharedElementName);
+ View view = sharedElement.first;
+ if (view == null) {
+ throw new IllegalArgumentException("Shared element must not be null");
+ }
+ views.add(sharedElement.first);
+ }
+ }
+
+ ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, window,
+ callback, names, names, views, false);
+ opts.mTransitionReceiver = exit;
+ opts.mSharedElementNames = names;
+ opts.mIsReturning = (activity == null);
+ if (activity == null) {
+ opts.mExitCoordinatorIndex = -1;
+ } else {
+ opts.mExitCoordinatorIndex =
+ activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
+ }
+ return exit;
+ }
+
+ /** @hide */
+ static ActivityOptions makeSceneTransitionAnimation(Activity activity,
+ ExitTransitionCoordinator exitCoordinator, ArrayList<String> sharedElementNames,
+ int resultCode, Intent resultData) {
+ ActivityOptions opts = new ActivityOptions();
+ opts.mAnimationType = ANIM_SCENE_TRANSITION;
+ opts.mSharedElementNames = sharedElementNames;
+ opts.mTransitionReceiver = exitCoordinator;
+ opts.mIsReturning = true;
+ opts.mResultCode = resultCode;
+ opts.mResultData = resultData;
+ opts.mExitCoordinatorIndex =
+ activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator);
+ return opts;
+ }
+
+ /**
+ * If set along with Intent.FLAG_ACTIVITY_NEW_DOCUMENT then the task being launched will not be
+ * presented to the user but will instead be only available through the recents task list.
+ * In addition, the new task wil be affiliated with the launching activity's task.
+ * Affiliated tasks are grouped together in the recents task list.
+ *
+ * <p>This behavior is not supported for activities with {@link
+ * android.R.styleable#AndroidManifestActivity_launchMode launchMode} values of
+ * <code>singleInstance</code> or <code>singleTask</code>.
+ */
+ public static ActivityOptions makeTaskLaunchBehind() {
+ final ActivityOptions opts = new ActivityOptions();
+ opts.mAnimationType = ANIM_LAUNCH_TASK_BEHIND;
+ return opts;
+ }
+
+ /**
+ * Create a basic ActivityOptions that has no special animation associated with it.
+ * Other options can still be set.
+ */
+ public static ActivityOptions makeBasic() {
+ final ActivityOptions opts = new ActivityOptions();
+ return opts;
+ }
+
+ /** @hide */
+ public boolean getLaunchTaskBehind() {
+ return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
+ }
+
+ private ActivityOptions() {
+ }
+
+ /** @hide */
+ public ActivityOptions(Bundle opts) {
+ // If the remote side sent us bad parcelables, they won't get the
+ // results they want, which is their loss.
+ opts.setDefusable(true);
+
+ mPackageName = opts.getString(KEY_PACKAGE_NAME);
+ try {
+ mUsageTimeReport = opts.getParcelable(KEY_USAGE_TIME_REPORT);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, e);
+ }
+ mLaunchBounds = opts.getParcelable(KEY_LAUNCH_BOUNDS);
+ mAnimationType = opts.getInt(KEY_ANIM_TYPE);
+ switch (mAnimationType) {
+ case ANIM_CUSTOM:
+ mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0);
+ mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0);
+ mAnimationStartedListener = IRemoteCallback.Stub.asInterface(
+ opts.getBinder(KEY_ANIM_START_LISTENER));
+ break;
+
+ case ANIM_CUSTOM_IN_PLACE:
+ mCustomInPlaceResId = opts.getInt(KEY_ANIM_IN_PLACE_RES_ID, 0);
+ break;
+
+ case ANIM_SCALE_UP:
+ case ANIM_CLIP_REVEAL:
+ mStartX = opts.getInt(KEY_ANIM_START_X, 0);
+ mStartY = opts.getInt(KEY_ANIM_START_Y, 0);
+ mWidth = opts.getInt(KEY_ANIM_WIDTH, 0);
+ mHeight = opts.getInt(KEY_ANIM_HEIGHT, 0);
+ break;
+
+ case ANIM_THUMBNAIL_SCALE_UP:
+ case ANIM_THUMBNAIL_SCALE_DOWN:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_UP:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_DOWN:
+ // Unpackage the GraphicBuffer from the parceled thumbnail
+ final GraphicBuffer buffer = opts.getParcelable(KEY_ANIM_THUMBNAIL);
+ if (buffer != null) {
+ mThumbnail = Bitmap.createHardwareBitmap(buffer);
+ }
+ mStartX = opts.getInt(KEY_ANIM_START_X, 0);
+ mStartY = opts.getInt(KEY_ANIM_START_Y, 0);
+ mWidth = opts.getInt(KEY_ANIM_WIDTH, 0);
+ mHeight = opts.getInt(KEY_ANIM_HEIGHT, 0);
+ mAnimationStartedListener = IRemoteCallback.Stub.asInterface(
+ opts.getBinder(KEY_ANIM_START_LISTENER));
+ break;
+
+ case ANIM_SCENE_TRANSITION:
+ mTransitionReceiver = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER);
+ mIsReturning = opts.getBoolean(KEY_TRANSITION_IS_RETURNING, false);
+ mSharedElementNames = opts.getStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS);
+ mResultData = opts.getParcelable(KEY_RESULT_DATA);
+ mResultCode = opts.getInt(KEY_RESULT_CODE);
+ mExitCoordinatorIndex = opts.getInt(KEY_EXIT_COORDINATOR_INDEX);
+ break;
+ }
+ mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY);
+ mLaunchStackId = opts.getInt(KEY_LAUNCH_STACK_ID, INVALID_STACK_ID);
+ mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
+ mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false);
+ mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false);
+ mDockCreateMode = opts.getInt(KEY_DOCK_CREATE_MODE, DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT);
+ mDisallowEnterPictureInPictureWhileLaunching = opts.getBoolean(
+ KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING, false);
+ if (opts.containsKey(KEY_ANIM_SPECS)) {
+ Parcelable[] specs = opts.getParcelableArray(KEY_ANIM_SPECS);
+ mAnimSpecs = new AppTransitionAnimationSpec[specs.length];
+ for (int i = specs.length - 1; i >= 0; i--) {
+ mAnimSpecs[i] = (AppTransitionAnimationSpec) specs[i];
+ }
+ }
+ if (opts.containsKey(KEY_ANIMATION_FINISHED_LISTENER)) {
+ mAnimationFinishedListener = IRemoteCallback.Stub.asInterface(
+ opts.getBinder(KEY_ANIMATION_FINISHED_LISTENER));
+ }
+ mRotationAnimationHint = opts.getInt(KEY_ROTATION_ANIMATION_HINT);
+ mAppVerificationBundle = opts.getBundle(KEY_INSTANT_APP_VERIFICATION_BUNDLE);
+ if (opts.containsKey(KEY_SPECS_FUTURE)) {
+ mSpecsFuture = IAppTransitionAnimationSpecsFuture.Stub.asInterface(opts.getBinder(
+ KEY_SPECS_FUTURE));
+ }
+ }
+
+ /**
+ * Sets the bounds (window size) that the activity should be launched in.
+ * Rect position should be provided in pixels and in screen coordinates.
+ * Set to null explicitly for fullscreen.
+ * <p>
+ * <strong>NOTE:<strong/> This value is ignored on devices that don't have
+ * {@link android.content.pm.PackageManager#FEATURE_FREEFORM_WINDOW_MANAGEMENT} or
+ * {@link android.content.pm.PackageManager#FEATURE_PICTURE_IN_PICTURE} enabled.
+ * @param screenSpacePixelRect Launch bounds to use for the activity or null for fullscreen.
+ */
+ public ActivityOptions setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
+ mLaunchBounds = screenSpacePixelRect != null ? new Rect(screenSpacePixelRect) : null;
+ return this;
+ }
+
+ /** @hide */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the bounds that should be used to launch the activity.
+ * @see #setLaunchBounds(Rect)
+ * @return Bounds used to launch the activity.
+ */
+ @Nullable
+ public Rect getLaunchBounds() {
+ return mLaunchBounds;
+ }
+
+ /** @hide */
+ public int getAnimationType() {
+ return mAnimationType;
+ }
+
+ /** @hide */
+ public int getCustomEnterResId() {
+ return mCustomEnterResId;
+ }
+
+ /** @hide */
+ public int getCustomExitResId() {
+ return mCustomExitResId;
+ }
+
+ /** @hide */
+ public int getCustomInPlaceResId() {
+ return mCustomInPlaceResId;
+ }
+
+ /**
+ * The thumbnail is copied into a hardware bitmap when it is bundled and sent to the system, so
+ * it should always be backed by a GraphicBuffer on the other end.
+ *
+ * @hide
+ */
+ public GraphicBuffer getThumbnail() {
+ return mThumbnail != null ? mThumbnail.createGraphicBufferHandle() : null;
+ }
+
+ /** @hide */
+ public int getStartX() {
+ return mStartX;
+ }
+
+ /** @hide */
+ public int getStartY() {
+ return mStartY;
+ }
+
+ /** @hide */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /** @hide */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /** @hide */
+ public IRemoteCallback getOnAnimationStartListener() {
+ return mAnimationStartedListener;
+ }
+
+ /** @hide */
+ public IRemoteCallback getAnimationFinishedListener() {
+ return mAnimationFinishedListener;
+ }
+
+ /** @hide */
+ public int getExitCoordinatorKey() { return mExitCoordinatorIndex; }
+
+ /** @hide */
+ public void abort() {
+ if (mAnimationStartedListener != null) {
+ try {
+ mAnimationStartedListener.sendResult(null);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /** @hide */
+ public boolean isReturning() {
+ return mIsReturning;
+ }
+
+ /**
+ * Returns whether or not the ActivityOptions was created with
+ * {@link #startSharedElementAnimation(Window, Pair[])}.
+ *
+ * @hide
+ */
+ boolean isCrossTask() {
+ return mExitCoordinatorIndex < 0;
+ }
+
+ /** @hide */
+ public ArrayList<String> getSharedElementNames() {
+ return mSharedElementNames;
+ }
+
+ /** @hide */
+ public ResultReceiver getResultReceiver() { return mTransitionReceiver; }
+
+ /** @hide */
+ public int getResultCode() { return mResultCode; }
+
+ /** @hide */
+ public Intent getResultData() { return mResultData; }
+
+ /** @hide */
+ public PendingIntent getUsageTimeReport() {
+ return mUsageTimeReport;
+ }
+
+ /** @hide */
+ public AppTransitionAnimationSpec[] getAnimSpecs() { return mAnimSpecs; }
+
+ /** @hide */
+ public IAppTransitionAnimationSpecsFuture getSpecsFuture() {
+ return mSpecsFuture;
+ }
+
+ /** @hide */
+ public static ActivityOptions fromBundle(Bundle bOptions) {
+ return bOptions != null ? new ActivityOptions(bOptions) : null;
+ }
+
+ /** @hide */
+ public static void abort(ActivityOptions options) {
+ if (options != null) {
+ options.abort();
+ }
+ }
+
+ /**
+ * Gets the id of the display where activity should be launched.
+ * @return The id of the display where activity should be launched,
+ * {@link android.view.Display#INVALID_DISPLAY} if not set.
+ * @see #setLaunchDisplayId(int)
+ */
+ public int getLaunchDisplayId() {
+ return mLaunchDisplayId;
+ }
+
+ /**
+ * Sets the id of the display where activity should be launched.
+ * An app can launch activities on public displays or private displays that are owned by the app
+ * or where an app already has activities. Otherwise, trying to launch on a private display
+ * or providing an invalid display id will result in an exception.
+ * <p>
+ * Setting launch display id will be ignored on devices that don't have
+ * {@link android.content.pm.PackageManager#FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS}.
+ * @param launchDisplayId The id of the display where the activity should be launched.
+ * @return {@code this} {@link ActivityOptions} instance.
+ */
+ public ActivityOptions setLaunchDisplayId(int launchDisplayId) {
+ mLaunchDisplayId = launchDisplayId;
+ return this;
+ }
+
+ /** @hide */
+ public int getLaunchStackId() {
+ return mLaunchStackId;
+ }
+
+ /** @hide */
+ @TestApi
+ public void setLaunchStackId(int launchStackId) {
+ mLaunchStackId = launchStackId;
+ }
+
+ /**
+ * Sets the task the activity will be launched in.
+ * @hide
+ */
+ @TestApi
+ public void setLaunchTaskId(int taskId) {
+ mLaunchTaskId = taskId;
+ }
+
+ /**
+ * @hide
+ */
+ public int getLaunchTaskId() {
+ return mLaunchTaskId;
+ }
+
+ /**
+ * Set's whether the activity launched with this option should be a task overlay. That is the
+ * activity will always be the top activity of the task. If {@param canResume} is true, then
+ * the task will also not be moved to the front of the stack.
+ * @hide
+ */
+ @TestApi
+ public void setTaskOverlay(boolean taskOverlay, boolean canResume) {
+ mTaskOverlay = taskOverlay;
+ mTaskOverlayCanResume = canResume;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean getTaskOverlay() {
+ return mTaskOverlay;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean canTaskOverlayResume() {
+ return mTaskOverlayCanResume;
+ }
+
+ /** @hide */
+ public int getDockCreateMode() {
+ return mDockCreateMode;
+ }
+
+ /** @hide */
+ public void setDockCreateMode(int dockCreateMode) {
+ mDockCreateMode = dockCreateMode;
+ }
+
+ /** @hide */
+ public void setDisallowEnterPictureInPictureWhileLaunching(boolean disallow) {
+ mDisallowEnterPictureInPictureWhileLaunching = disallow;
+ }
+
+ /** @hide */
+ public boolean disallowEnterPictureInPictureWhileLaunching() {
+ return mDisallowEnterPictureInPictureWhileLaunching;
+ }
+
+ /**
+ * Update the current values in this ActivityOptions from those supplied
+ * in <var>otherOptions</var>. Any values
+ * defined in <var>otherOptions</var> replace those in the base options.
+ */
+ public void update(ActivityOptions otherOptions) {
+ if (otherOptions.mPackageName != null) {
+ mPackageName = otherOptions.mPackageName;
+ }
+ mUsageTimeReport = otherOptions.mUsageTimeReport;
+ mTransitionReceiver = null;
+ mSharedElementNames = null;
+ mIsReturning = false;
+ mResultData = null;
+ mResultCode = 0;
+ mExitCoordinatorIndex = 0;
+ mAnimationType = otherOptions.mAnimationType;
+ switch (otherOptions.mAnimationType) {
+ case ANIM_CUSTOM:
+ mCustomEnterResId = otherOptions.mCustomEnterResId;
+ mCustomExitResId = otherOptions.mCustomExitResId;
+ mThumbnail = null;
+ if (mAnimationStartedListener != null) {
+ try {
+ mAnimationStartedListener.sendResult(null);
+ } catch (RemoteException e) {
+ }
+ }
+ mAnimationStartedListener = otherOptions.mAnimationStartedListener;
+ break;
+ case ANIM_CUSTOM_IN_PLACE:
+ mCustomInPlaceResId = otherOptions.mCustomInPlaceResId;
+ break;
+ case ANIM_SCALE_UP:
+ mStartX = otherOptions.mStartX;
+ mStartY = otherOptions.mStartY;
+ mWidth = otherOptions.mWidth;
+ mHeight = otherOptions.mHeight;
+ if (mAnimationStartedListener != null) {
+ try {
+ mAnimationStartedListener.sendResult(null);
+ } catch (RemoteException e) {
+ }
+ }
+ mAnimationStartedListener = null;
+ break;
+ case ANIM_THUMBNAIL_SCALE_UP:
+ case ANIM_THUMBNAIL_SCALE_DOWN:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_UP:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_DOWN:
+ mThumbnail = otherOptions.mThumbnail;
+ mStartX = otherOptions.mStartX;
+ mStartY = otherOptions.mStartY;
+ mWidth = otherOptions.mWidth;
+ mHeight = otherOptions.mHeight;
+ if (mAnimationStartedListener != null) {
+ try {
+ mAnimationStartedListener.sendResult(null);
+ } catch (RemoteException e) {
+ }
+ }
+ mAnimationStartedListener = otherOptions.mAnimationStartedListener;
+ break;
+ case ANIM_SCENE_TRANSITION:
+ mTransitionReceiver = otherOptions.mTransitionReceiver;
+ mSharedElementNames = otherOptions.mSharedElementNames;
+ mIsReturning = otherOptions.mIsReturning;
+ mThumbnail = null;
+ mAnimationStartedListener = null;
+ mResultData = otherOptions.mResultData;
+ mResultCode = otherOptions.mResultCode;
+ mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex;
+ break;
+ }
+ mAnimSpecs = otherOptions.mAnimSpecs;
+ mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
+ mSpecsFuture = otherOptions.mSpecsFuture;
+ }
+
+ /**
+ * Returns the created options as a Bundle, which can be passed to
+ * {@link android.content.Context#startActivity(android.content.Intent, android.os.Bundle)
+ * Context.startActivity(Intent, Bundle)} and related methods.
+ * Note that the returned Bundle is still owned by the ActivityOptions
+ * object; you must not modify it, but can supply it to the startActivity
+ * methods that take an options Bundle.
+ */
+ public Bundle toBundle() {
+ Bundle b = new Bundle();
+ if (mPackageName != null) {
+ b.putString(KEY_PACKAGE_NAME, mPackageName);
+ }
+ if (mLaunchBounds != null) {
+ b.putParcelable(KEY_LAUNCH_BOUNDS, mLaunchBounds);
+ }
+ b.putInt(KEY_ANIM_TYPE, mAnimationType);
+ if (mUsageTimeReport != null) {
+ b.putParcelable(KEY_USAGE_TIME_REPORT, mUsageTimeReport);
+ }
+ switch (mAnimationType) {
+ case ANIM_CUSTOM:
+ b.putInt(KEY_ANIM_ENTER_RES_ID, mCustomEnterResId);
+ b.putInt(KEY_ANIM_EXIT_RES_ID, mCustomExitResId);
+ b.putBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener
+ != null ? mAnimationStartedListener.asBinder() : null);
+ break;
+ case ANIM_CUSTOM_IN_PLACE:
+ b.putInt(KEY_ANIM_IN_PLACE_RES_ID, mCustomInPlaceResId);
+ break;
+ case ANIM_SCALE_UP:
+ case ANIM_CLIP_REVEAL:
+ b.putInt(KEY_ANIM_START_X, mStartX);
+ b.putInt(KEY_ANIM_START_Y, mStartY);
+ b.putInt(KEY_ANIM_WIDTH, mWidth);
+ b.putInt(KEY_ANIM_HEIGHT, mHeight);
+ break;
+ case ANIM_THUMBNAIL_SCALE_UP:
+ case ANIM_THUMBNAIL_SCALE_DOWN:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_UP:
+ case ANIM_THUMBNAIL_ASPECT_SCALE_DOWN:
+ // Once we parcel the thumbnail for transfering over to the system, create a copy of
+ // the bitmap to a hardware bitmap and pass through the GraphicBuffer
+ if (mThumbnail != null) {
+ final Bitmap hwBitmap = mThumbnail.copy(Config.HARDWARE, false /* isMutable */);
+ if (hwBitmap != null) {
+ b.putParcelable(KEY_ANIM_THUMBNAIL, hwBitmap.createGraphicBufferHandle());
+ } else {
+ Slog.w(TAG, "Failed to copy thumbnail");
+ }
+ }
+ b.putInt(KEY_ANIM_START_X, mStartX);
+ b.putInt(KEY_ANIM_START_Y, mStartY);
+ b.putInt(KEY_ANIM_WIDTH, mWidth);
+ b.putInt(KEY_ANIM_HEIGHT, mHeight);
+ b.putBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener
+ != null ? mAnimationStartedListener.asBinder() : null);
+ break;
+ case ANIM_SCENE_TRANSITION:
+ if (mTransitionReceiver != null) {
+ b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mTransitionReceiver);
+ }
+ b.putBoolean(KEY_TRANSITION_IS_RETURNING, mIsReturning);
+ b.putStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS, mSharedElementNames);
+ b.putParcelable(KEY_RESULT_DATA, mResultData);
+ b.putInt(KEY_RESULT_CODE, mResultCode);
+ b.putInt(KEY_EXIT_COORDINATOR_INDEX, mExitCoordinatorIndex);
+ break;
+ }
+ b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId);
+ b.putInt(KEY_LAUNCH_STACK_ID, mLaunchStackId);
+ b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId);
+ b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay);
+ b.putBoolean(KEY_TASK_OVERLAY_CAN_RESUME, mTaskOverlayCanResume);
+ b.putInt(KEY_DOCK_CREATE_MODE, mDockCreateMode);
+ b.putBoolean(KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING,
+ mDisallowEnterPictureInPictureWhileLaunching);
+ if (mAnimSpecs != null) {
+ b.putParcelableArray(KEY_ANIM_SPECS, mAnimSpecs);
+ }
+ if (mAnimationFinishedListener != null) {
+ b.putBinder(KEY_ANIMATION_FINISHED_LISTENER, mAnimationFinishedListener.asBinder());
+ }
+ if (mSpecsFuture != null) {
+ b.putBinder(KEY_SPECS_FUTURE, mSpecsFuture.asBinder());
+ }
+ b.putInt(KEY_ROTATION_ANIMATION_HINT, mRotationAnimationHint);
+ if (mAppVerificationBundle != null) {
+ b.putBundle(KEY_INSTANT_APP_VERIFICATION_BUNDLE, mAppVerificationBundle);
+ }
+
+ return b;
+ }
+
+ /**
+ * Ask the the system track that time the user spends in the app being launched, and
+ * report it back once done. The report will be sent to the given receiver, with
+ * the extras {@link #EXTRA_USAGE_TIME_REPORT} and {@link #EXTRA_USAGE_TIME_REPORT_PACKAGES}
+ * filled in.
+ *
+ * <p>The time interval tracked is from launching this activity until the user leaves
+ * that activity's flow. They are considered to stay in the flow as long as
+ * new activities are being launched or returned to from the original flow,
+ * even if this crosses package or task boundaries. For example, if the originator
+ * starts an activity to view an image, and while there the user selects to share,
+ * which launches their email app in a new task, and they complete the share, the
+ * time during that entire operation will be included until they finally hit back from
+ * the original image viewer activity.</p>
+ *
+ * <p>The user is considered to complete a flow once they switch to another
+ * activity that is not part of the tracked flow. This may happen, for example, by
+ * using the notification shade, launcher, or recents to launch or switch to another
+ * app. Simply going in to these navigation elements does not break the flow (although
+ * the launcher and recents stops time tracking of the session); it is the act of
+ * going somewhere else that completes the tracking.</p>
+ *
+ * @param receiver A broadcast receiver that willl receive the report.
+ */
+ public void requestUsageTimeReport(PendingIntent receiver) {
+ mUsageTimeReport = receiver;
+ }
+
+ /**
+ * Return the filtered options only meant to be seen by the target activity itself
+ * @hide
+ */
+ public ActivityOptions forTargetActivity() {
+ if (mAnimationType == ANIM_SCENE_TRANSITION) {
+ final ActivityOptions result = new ActivityOptions();
+ result.update(this);
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the rotation animation set by {@link setRotationAnimationHint} or -1
+ * if unspecified.
+ * @hide
+ */
+ public int getRotationAnimationHint() {
+ return mRotationAnimationHint;
+ }
+
+
+ /**
+ * Set a rotation animation to be used if launching the activity
+ * triggers an orientation change, or -1 to clear. See
+ * {@link android.view.WindowManager.LayoutParams} for rotation
+ * animation values.
+ * @hide
+ */
+ public void setRotationAnimationHint(int hint) {
+ mRotationAnimationHint = hint;
+ }
+
+ /**
+ * Pop the extra verification bundle for the installer.
+ * This removes the bundle from the ActivityOptions to make sure the installer bundle
+ * is only available once.
+ * @hide
+ */
+ public Bundle popAppVerificationBundle() {
+ Bundle out = mAppVerificationBundle;
+ mAppVerificationBundle = null;
+ return out;
+ }
+
+ /**
+ * Set the {@link Bundle} that is provided to the app installer for additional verification
+ * if the call to {@link Context#startActivity} results in an app being installed.
+ *
+ * This Bundle is not provided to any other app besides the installer.
+ */
+ public ActivityOptions setAppVerificationBundle(Bundle bundle) {
+ mAppVerificationBundle = bundle;
+ return this;
+
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return "ActivityOptions(" + hashCode() + "), mPackageName=" + mPackageName
+ + ", mAnimationType=" + mAnimationType + ", mStartX=" + mStartX + ", mStartY="
+ + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight;
+ }
+
+ private static class HideWindowListener extends TransitionListenerAdapter
+ implements ExitTransitionCoordinator.HideSharedElementsCallback {
+ private final Window mWindow;
+ private final ExitTransitionCoordinator mExit;
+ private final boolean mWaitingForTransition;
+ private boolean mTransitionEnded;
+ private boolean mSharedElementHidden;
+ private ArrayList<View> mSharedElements;
+
+ public HideWindowListener(Window window, ExitTransitionCoordinator exit) {
+ mWindow = window;
+ mExit = exit;
+ mSharedElements = new ArrayList<>(exit.mSharedElements);
+ Transition transition = mWindow.getExitTransition();
+ if (transition != null) {
+ transition.addListener(this);
+ mWaitingForTransition = true;
+ } else {
+ mWaitingForTransition = false;
+ }
+ View decorView = mWindow.getDecorView();
+ if (decorView != null) {
+ if (decorView.getTag(com.android.internal.R.id.cross_task_transition) != null) {
+ throw new IllegalStateException(
+ "Cannot start a transition while one is running");
+ }
+ decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, exit);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ mTransitionEnded = true;
+ hideWhenDone();
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void hideSharedElements() {
+ mSharedElementHidden = true;
+ hideWhenDone();
+ }
+
+ private void hideWhenDone() {
+ if (mSharedElementHidden && (!mWaitingForTransition || mTransitionEnded)) {
+ mExit.resetViews();
+ int numSharedElements = mSharedElements.size();
+ for (int i = 0; i < numSharedElements; i++) {
+ View view = mSharedElements.get(i);
+ view.requestLayout();
+ }
+ View decorView = mWindow.getDecorView();
+ if (decorView != null) {
+ decorView.setTagInternal(
+ com.android.internal.R.id.cross_task_transition, null);
+ decorView.setVisibility(View.GONE);
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
new file mode 100644
index 00000000..4e8d2400
--- /dev/null
+++ b/android/app/ActivityThread.java
@@ -0,0 +1,6527 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.assist.AssistContent;
+import android.app.assist.AssistStructure;
+import android.app.backup.BackupAgent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks2;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.AssetManager;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDebug;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.hardware.display.DisplayManagerGlobal;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.Network;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.DropBoxManager;
+import android.os.Environment;
+import android.os.GraphicsEnvironment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.TransactionTooLargeException;
+import android.os.UserHandle;
+import android.provider.BlockedNumberContract;
+import android.provider.CalendarContract;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.provider.Downloads;
+import android.provider.FontsContract;
+import android.provider.Settings;
+import android.renderscript.RenderScriptCacheDir;
+import android.security.NetworkSecurityPolicy;
+import android.security.net.config.NetworkSecurityConfigProvider;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.DisplayMetrics;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.LogPrinter;
+import android.util.LogWriter;
+import android.util.Pair;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.util.SuperNotCalledException;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.ThreadedRenderer;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewManager;
+import android.view.ViewRootImpl;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.webkit.WebView;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IVoiceInteractor;
+import com.android.internal.content.ReferrerIntent;
+import com.android.internal.os.BinderInternal;
+import com.android.internal.os.RuntimeInit;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+import com.android.org.conscrypt.TrustedCertificateStore;
+
+import dalvik.system.BaseDexClassLoader;
+import dalvik.system.CloseGuard;
+import dalvik.system.VMDebug;
+import dalvik.system.VMRuntime;
+
+import libcore.io.DropBox;
+import libcore.io.EventLogger;
+import libcore.io.IoUtils;
+import libcore.net.event.NetworkEventDispatcher;
+
+import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
+
+import java.io.File;
+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.Field;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TimeZone;
+
+final class RemoteServiceException extends AndroidRuntimeException {
+ public RemoteServiceException(String msg) {
+ super(msg);
+ }
+}
+
+/**
+ * This manages the execution of the main thread in an
+ * application process, scheduling and executing activities,
+ * broadcasts, and other operations on it as the activity
+ * manager requests.
+ *
+ * {@hide}
+ */
+public final class ActivityThread {
+ /** @hide */
+ public static final String TAG = "ActivityThread";
+ private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565;
+ static final boolean localLOGV = false;
+ static final boolean DEBUG_MESSAGES = false;
+ /** @hide */
+ public static final boolean DEBUG_BROADCAST = false;
+ private static final boolean DEBUG_RESULTS = false;
+ private static final boolean DEBUG_BACKUP = false;
+ public static final boolean DEBUG_CONFIGURATION = false;
+ private static final boolean DEBUG_SERVICE = false;
+ private static final boolean DEBUG_MEMORY_TRIM = false;
+ private static final boolean DEBUG_PROVIDER = false;
+ private static final boolean DEBUG_ORDER = false;
+ private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
+ private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003;
+ private static final int LOG_AM_ON_PAUSE_CALLED = 30021;
+ private static final int LOG_AM_ON_RESUME_CALLED = 30022;
+ private static final int LOG_AM_ON_STOP_CALLED = 30049;
+
+ /** Type for IActivityManager.serviceDoneExecuting: anonymous operation */
+ public static final int SERVICE_DONE_EXECUTING_ANON = 0;
+ /** Type for IActivityManager.serviceDoneExecuting: done with an onStart call */
+ public static final int SERVICE_DONE_EXECUTING_START = 1;
+ /** Type for IActivityManager.serviceDoneExecuting: done stopping (destroying) service */
+ public static final int SERVICE_DONE_EXECUTING_STOP = 2;
+
+ // Details for pausing activity.
+ private static final int USER_LEAVING = 1;
+ private static final int DONT_REPORT = 2;
+
+ // Whether to invoke an activity callback after delivering new configuration.
+ private static final boolean REPORT_TO_ACTIVITY = true;
+
+ /**
+ * Denotes an invalid sequence number corresponding to a process state change.
+ */
+ public static final long INVALID_PROC_STATE_SEQ = -1;
+
+ private final Object mNetworkPolicyLock = new Object();
+
+ /**
+ * Denotes the sequence number of the process state change for which the main thread needs
+ * to block until the network rules are updated for it.
+ *
+ * Value of {@link #INVALID_PROC_STATE_SEQ} indicates there is no need for blocking.
+ */
+ @GuardedBy("mNetworkPolicyLock")
+ private long mNetworkBlockSeq = INVALID_PROC_STATE_SEQ;
+
+ private ContextImpl mSystemContext;
+ private ContextImpl mSystemUiContext;
+
+ static volatile IPackageManager sPackageManager;
+
+ final ApplicationThread mAppThread = new ApplicationThread();
+ final Looper mLooper = Looper.myLooper();
+ final H mH = new H();
+ final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
+ // List of new activities (via ActivityRecord.nextIdle) that should
+ // be reported when next we idle.
+ ActivityClientRecord mNewActivities = null;
+ // Number of activities that are currently visible on-screen.
+ int mNumVisibleActivities = 0;
+ ArrayList<WeakReference<AssistStructure>> mLastAssistStructures = new ArrayList<>();
+ private int mLastSessionId;
+ final ArrayMap<IBinder, Service> mServices = new ArrayMap<>();
+ AppBindData mBoundApplication;
+ Profiler mProfiler;
+ int mCurDefaultDisplayDpi;
+ boolean mDensityCompatMode;
+ Configuration mConfiguration;
+ Configuration mCompatConfiguration;
+ Application mInitialApplication;
+ final ArrayList<Application> mAllApplications
+ = new ArrayList<Application>();
+ // set of instantiated backup agents, keyed by package name
+ final ArrayMap<String, BackupAgent> mBackupAgents = new ArrayMap<String, BackupAgent>();
+ /** Reference to singleton {@link ActivityThread} */
+ private static volatile ActivityThread sCurrentActivityThread;
+ Instrumentation mInstrumentation;
+ String mInstrumentationPackageName = null;
+ String mInstrumentationAppDir = null;
+ String[] mInstrumentationSplitAppDirs = null;
+ String mInstrumentationLibDir = null;
+ String mInstrumentedAppDir = null;
+ String[] mInstrumentedSplitAppDirs = null;
+ String mInstrumentedLibDir = null;
+ boolean mSystemThread = false;
+ boolean mJitEnabled = false;
+ boolean mSomeActivitiesChanged = false;
+ boolean mUpdatingSystemConfig = false;
+
+ // These can be accessed by multiple threads; mResourcesManager is the lock.
+ // XXX For now we keep around information about all packages we have
+ // seen, not removing entries from this map.
+ // NOTE: The activity and window managers need to call in to
+ // ActivityThread to do things like update resource configurations,
+ // which means this lock gets held while the activity and window managers
+ // holds their own lock. Thus you MUST NEVER call back into the activity manager
+ // or window manager or anything that depends on them while holding this lock.
+ // These LoadedApk are only valid for the userId that we're running as.
+ @GuardedBy("mResourcesManager")
+ final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
+ @GuardedBy("mResourcesManager")
+ final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages = new ArrayMap<>();
+ @GuardedBy("mResourcesManager")
+ final ArrayList<ActivityClientRecord> mRelaunchingActivities = new ArrayList<>();
+ @GuardedBy("mResourcesManager")
+ Configuration mPendingConfiguration = null;
+ // Because we merge activity relaunch operations we can't depend on the ordering provided by
+ // the handler messages. We need to introduce secondary ordering mechanism, which will allow
+ // us to drop certain events, if we know that they happened before relaunch we already executed.
+ // This represents the order of receiving the request from AM.
+ @GuardedBy("mResourcesManager")
+ int mLifecycleSeq = 0;
+
+ private final ResourcesManager mResourcesManager;
+
+ private static final class ProviderKey {
+ final String authority;
+ final int userId;
+
+ public ProviderKey(String authority, int userId) {
+ this.authority = authority;
+ this.userId = userId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ProviderKey) {
+ final ProviderKey other = (ProviderKey) o;
+ return Objects.equals(authority, other.authority) && userId == other.userId;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return ((authority != null) ? authority.hashCode() : 0) ^ userId;
+ }
+ }
+
+ // The lock of mProviderMap protects the following variables.
+ final ArrayMap<ProviderKey, ProviderClientRecord> mProviderMap
+ = new ArrayMap<ProviderKey, ProviderClientRecord>();
+ final ArrayMap<IBinder, ProviderRefCount> mProviderRefCountMap
+ = new ArrayMap<IBinder, ProviderRefCount>();
+ final ArrayMap<IBinder, ProviderClientRecord> mLocalProviders
+ = new ArrayMap<IBinder, ProviderClientRecord>();
+ final ArrayMap<ComponentName, ProviderClientRecord> mLocalProvidersByName
+ = new ArrayMap<ComponentName, ProviderClientRecord>();
+
+ final ArrayMap<Activity, ArrayList<OnActivityPausedListener>> mOnPauseListeners
+ = new ArrayMap<Activity, ArrayList<OnActivityPausedListener>>();
+
+ final GcIdler mGcIdler = new GcIdler();
+ boolean mGcIdlerScheduled = false;
+
+ static volatile Handler sMainThreadHandler; // set once in main()
+
+ Bundle mCoreSettings = null;
+
+ static final class ActivityClientRecord {
+ IBinder token;
+ int ident;
+ Intent intent;
+ String referrer;
+ IVoiceInteractor voiceInteractor;
+ Bundle state;
+ PersistableBundle persistentState;
+ Activity activity;
+ Window window;
+ Activity parent;
+ String embeddedID;
+ Activity.NonConfigurationInstances lastNonConfigurationInstances;
+ boolean paused;
+ boolean stopped;
+ boolean hideForNow;
+ Configuration newConfig;
+ Configuration createdConfig;
+ Configuration overrideConfig;
+ // Used for consolidating configs before sending on to Activity.
+ private Configuration tmpConfig = new Configuration();
+ // Callback used for updating activity override config.
+ ViewRootImpl.ActivityConfigCallback configCallback;
+ ActivityClientRecord nextIdle;
+
+ ProfilerInfo profilerInfo;
+
+ ActivityInfo activityInfo;
+ CompatibilityInfo compatInfo;
+ LoadedApk packageInfo;
+
+ List<ResultInfo> pendingResults;
+ List<ReferrerIntent> pendingIntents;
+
+ boolean startsNotResumed;
+ boolean isForward;
+ int pendingConfigChanges;
+ boolean onlyLocalRequest;
+
+ Window mPendingRemoveWindow;
+ WindowManager mPendingRemoveWindowManager;
+ boolean mPreserveWindow;
+
+ // Set for relaunch requests, indicates the order number of the relaunch operation, so it
+ // can be compared with other lifecycle operations.
+ int relaunchSeq = 0;
+
+ // Can only be accessed from the UI thread. This represents the latest processed message
+ // that is related to lifecycle events/
+ int lastProcessedSeq = 0;
+
+ ActivityClientRecord() {
+ parent = null;
+ embeddedID = null;
+ paused = false;
+ stopped = false;
+ hideForNow = false;
+ nextIdle = null;
+ configCallback = (Configuration overrideConfig, int newDisplayId) -> {
+ if (activity == null) {
+ throw new IllegalStateException(
+ "Received config update for non-existing activity");
+ }
+ activity.mMainThread.handleActivityConfigurationChanged(
+ new ActivityConfigChangeData(token, overrideConfig), newDisplayId);
+ };
+ }
+
+ public boolean isPreHoneycomb() {
+ if (activity != null) {
+ return activity.getApplicationInfo().targetSdkVersion
+ < android.os.Build.VERSION_CODES.HONEYCOMB;
+ }
+ return false;
+ }
+
+ public boolean isPersistable() {
+ return activityInfo.persistableMode == ActivityInfo.PERSIST_ACROSS_REBOOTS;
+ }
+
+ public String toString() {
+ ComponentName componentName = intent != null ? intent.getComponent() : null;
+ return "ActivityRecord{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " token=" + token + " " + (componentName == null
+ ? "no component name" : componentName.toShortString())
+ + "}";
+ }
+
+ public String getStateString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("ActivityClientRecord{");
+ sb.append("paused=").append(paused);
+ sb.append(", stopped=").append(stopped);
+ sb.append(", hideForNow=").append(hideForNow);
+ sb.append(", startsNotResumed=").append(startsNotResumed);
+ sb.append(", isForward=").append(isForward);
+ sb.append(", pendingConfigChanges=").append(pendingConfigChanges);
+ sb.append(", onlyLocalRequest=").append(onlyLocalRequest);
+ sb.append(", preserveWindow=").append(mPreserveWindow);
+ if (activity != null) {
+ sb.append(", Activity{");
+ sb.append("resumed=").append(activity.mResumed);
+ sb.append(", stopped=").append(activity.mStopped);
+ sb.append(", finished=").append(activity.isFinishing());
+ sb.append(", destroyed=").append(activity.isDestroyed());
+ sb.append(", startedActivity=").append(activity.mStartedActivity);
+ sb.append(", temporaryPause=").append(activity.mTemporaryPause);
+ sb.append(", changingConfigurations=").append(activity.mChangingConfigurations);
+ sb.append("}");
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+ }
+
+ final class ProviderClientRecord {
+ final String[] mNames;
+ final IContentProvider mProvider;
+ final ContentProvider mLocalProvider;
+ final ContentProviderHolder mHolder;
+
+ ProviderClientRecord(String[] names, IContentProvider provider,
+ ContentProvider localProvider, ContentProviderHolder holder) {
+ mNames = names;
+ mProvider = provider;
+ mLocalProvider = localProvider;
+ mHolder = holder;
+ }
+ }
+
+ static final class NewIntentData {
+ List<ReferrerIntent> intents;
+ IBinder token;
+ boolean andPause;
+ public String toString() {
+ return "NewIntentData{intents=" + intents + " token=" + token
+ + " andPause=" + andPause +"}";
+ }
+ }
+
+ static final class ReceiverData extends BroadcastReceiver.PendingResult {
+ public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras,
+ boolean ordered, boolean sticky, IBinder token, int sendingUser) {
+ super(resultCode, resultData, resultExtras, TYPE_COMPONENT, ordered, sticky,
+ token, sendingUser, intent.getFlags());
+ this.intent = intent;
+ }
+
+ Intent intent;
+ ActivityInfo info;
+ CompatibilityInfo compatInfo;
+ public String toString() {
+ return "ReceiverData{intent=" + intent + " packageName=" +
+ info.packageName + " resultCode=" + getResultCode()
+ + " resultData=" + getResultData() + " resultExtras="
+ + getResultExtras(false) + "}";
+ }
+ }
+
+ static final class CreateBackupAgentData {
+ ApplicationInfo appInfo;
+ CompatibilityInfo compatInfo;
+ int backupMode;
+ public String toString() {
+ return "CreateBackupAgentData{appInfo=" + appInfo
+ + " backupAgent=" + appInfo.backupAgentName
+ + " mode=" + backupMode + "}";
+ }
+ }
+
+ static final class CreateServiceData {
+ IBinder token;
+ ServiceInfo info;
+ CompatibilityInfo compatInfo;
+ Intent intent;
+ public String toString() {
+ return "CreateServiceData{token=" + token + " className="
+ + info.name + " packageName=" + info.packageName
+ + " intent=" + intent + "}";
+ }
+ }
+
+ static final class BindServiceData {
+ IBinder token;
+ Intent intent;
+ boolean rebind;
+ public String toString() {
+ return "BindServiceData{token=" + token + " intent=" + intent + "}";
+ }
+ }
+
+ static final class ServiceArgsData {
+ IBinder token;
+ boolean taskRemoved;
+ int startId;
+ int flags;
+ Intent args;
+ public String toString() {
+ return "ServiceArgsData{token=" + token + " startId=" + startId
+ + " args=" + args + "}";
+ }
+ }
+
+ static final class AppBindData {
+ LoadedApk info;
+ String processName;
+ ApplicationInfo appInfo;
+ List<ProviderInfo> providers;
+ ComponentName instrumentationName;
+ Bundle instrumentationArgs;
+ IInstrumentationWatcher instrumentationWatcher;
+ IUiAutomationConnection instrumentationUiAutomationConnection;
+ int debugMode;
+ boolean enableBinderTracking;
+ boolean trackAllocation;
+ boolean restrictedBackupMode;
+ boolean persistent;
+ Configuration config;
+ CompatibilityInfo compatInfo;
+ String buildSerial;
+
+ /** Initial values for {@link Profiler}. */
+ ProfilerInfo initProfilerInfo;
+
+ public String toString() {
+ return "AppBindData{appInfo=" + appInfo + "}";
+ }
+ }
+
+ static final class Profiler {
+ String profileFile;
+ ParcelFileDescriptor profileFd;
+ int samplingInterval;
+ boolean autoStopProfiler;
+ boolean streamingOutput;
+ boolean profiling;
+ boolean handlingProfiling;
+ public void setProfiler(ProfilerInfo profilerInfo) {
+ ParcelFileDescriptor fd = profilerInfo.profileFd;
+ if (profiling) {
+ if (fd != null) {
+ try {
+ fd.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ return;
+ }
+ if (profileFd != null) {
+ try {
+ profileFd.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ profileFile = profilerInfo.profileFile;
+ profileFd = fd;
+ samplingInterval = profilerInfo.samplingInterval;
+ autoStopProfiler = profilerInfo.autoStopProfiler;
+ streamingOutput = profilerInfo.streamingOutput;
+ }
+ public void startProfiling() {
+ if (profileFd == null || profiling) {
+ return;
+ }
+ try {
+ int bufferSize = SystemProperties.getInt("debug.traceview-buffer-size-mb", 8);
+ VMDebug.startMethodTracing(profileFile, profileFd.getFileDescriptor(),
+ bufferSize * 1024 * 1024, 0, samplingInterval != 0, samplingInterval,
+ streamingOutput);
+ profiling = true;
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Profiling failed on path " + profileFile);
+ try {
+ profileFd.close();
+ profileFd = null;
+ } catch (IOException e2) {
+ Slog.w(TAG, "Failure closing profile fd", e2);
+ }
+ }
+ }
+ public void stopProfiling() {
+ if (profiling) {
+ profiling = false;
+ Debug.stopMethodTracing();
+ if (profileFd != null) {
+ try {
+ profileFd.close();
+ } catch (IOException e) {
+ }
+ }
+ profileFd = null;
+ profileFile = null;
+ }
+ }
+ }
+
+ static final class DumpComponentInfo {
+ ParcelFileDescriptor fd;
+ IBinder token;
+ String prefix;
+ String[] args;
+ }
+
+ static final class ResultData {
+ IBinder token;
+ List<ResultInfo> results;
+ public String toString() {
+ return "ResultData{token=" + token + " results" + results + "}";
+ }
+ }
+
+ static final class ContextCleanupInfo {
+ ContextImpl context;
+ String what;
+ String who;
+ }
+
+ static final class DumpHeapData {
+ public boolean managed;
+ public boolean mallocInfo;
+ public boolean runGc;
+ String path;
+ ParcelFileDescriptor fd;
+ }
+
+ static final class UpdateCompatibilityData {
+ String pkg;
+ CompatibilityInfo info;
+ }
+
+ static final class RequestAssistContextExtras {
+ IBinder activityToken;
+ IBinder requestToken;
+ int requestType;
+ int sessionId;
+ int flags;
+ }
+
+ static final class ActivityConfigChangeData {
+ final IBinder activityToken;
+ final Configuration overrideConfig;
+ public ActivityConfigChangeData(IBinder token, Configuration config) {
+ activityToken = token;
+ overrideConfig = config;
+ }
+ }
+
+ private class ApplicationThread extends IApplicationThread.Stub {
+ private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s";
+
+ private int mLastProcessState = -1;
+
+ private void updatePendingConfiguration(Configuration config) {
+ synchronized (mResourcesManager) {
+ if (mPendingConfiguration == null ||
+ mPendingConfiguration.isOtherSeqNewer(config)) {
+ mPendingConfiguration = config;
+ }
+ }
+ }
+
+ public final void schedulePauseActivity(IBinder token, boolean finished,
+ boolean userLeaving, int configChanges, boolean dontReport) {
+ int seq = getLifecycleSeq();
+ if (DEBUG_ORDER) Slog.d(TAG, "pauseActivity " + ActivityThread.this
+ + " operation received seq: " + seq);
+ sendMessage(
+ finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY,
+ token,
+ (userLeaving ? USER_LEAVING : 0) | (dontReport ? DONT_REPORT : 0),
+ configChanges,
+ seq);
+ }
+
+ public final void scheduleStopActivity(IBinder token, boolean showWindow,
+ int configChanges) {
+ int seq = getLifecycleSeq();
+ if (DEBUG_ORDER) Slog.d(TAG, "stopActivity " + ActivityThread.this
+ + " operation received seq: " + seq);
+ sendMessage(
+ showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
+ token, 0, configChanges, seq);
+ }
+
+ public final void scheduleWindowVisibility(IBinder token, boolean showWindow) {
+ sendMessage(
+ showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW,
+ token);
+ }
+
+ public final void scheduleSleeping(IBinder token, boolean sleeping) {
+ sendMessage(H.SLEEPING, token, sleeping ? 1 : 0);
+ }
+
+ public final void scheduleResumeActivity(IBinder token, int processState,
+ boolean isForward, Bundle resumeArgs) {
+ int seq = getLifecycleSeq();
+ if (DEBUG_ORDER) Slog.d(TAG, "resumeActivity " + ActivityThread.this
+ + " operation received seq: " + seq);
+ updateProcessState(processState, false);
+ sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0, 0, seq);
+ }
+
+ public final void scheduleSendResult(IBinder token, List<ResultInfo> results) {
+ ResultData res = new ResultData();
+ res.token = token;
+ res.results = results;
+ sendMessage(H.SEND_RESULT, res);
+ }
+
+ // we use token to identify this activity without having to send the
+ // activity itself back to the activity manager. (matters more with ipc)
+ @Override
+ public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
+ ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
+ CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
+ int procState, Bundle state, PersistableBundle persistentState,
+ List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
+ boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
+
+ updateProcessState(procState, false);
+
+ ActivityClientRecord r = new ActivityClientRecord();
+
+ r.token = token;
+ r.ident = ident;
+ r.intent = intent;
+ r.referrer = referrer;
+ r.voiceInteractor = voiceInteractor;
+ r.activityInfo = info;
+ r.compatInfo = compatInfo;
+ r.state = state;
+ r.persistentState = persistentState;
+
+ r.pendingResults = pendingResults;
+ r.pendingIntents = pendingNewIntents;
+
+ r.startsNotResumed = notResumed;
+ r.isForward = isForward;
+
+ r.profilerInfo = profilerInfo;
+
+ r.overrideConfig = overrideConfig;
+ updatePendingConfiguration(curConfig);
+
+ sendMessage(H.LAUNCH_ACTIVITY, r);
+ }
+
+ @Override
+ public final void scheduleRelaunchActivity(IBinder token,
+ List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
+ int configChanges, boolean notResumed, Configuration config,
+ Configuration overrideConfig, boolean preserveWindow) {
+ requestRelaunchActivity(token, pendingResults, pendingNewIntents,
+ configChanges, notResumed, config, overrideConfig, true, preserveWindow);
+ }
+
+ public final void scheduleNewIntent(
+ List<ReferrerIntent> intents, IBinder token, boolean andPause) {
+ NewIntentData data = new NewIntentData();
+ data.intents = intents;
+ data.token = token;
+ data.andPause = andPause;
+
+ sendMessage(H.NEW_INTENT, data);
+ }
+
+ public final void scheduleDestroyActivity(IBinder token, boolean finishing,
+ int configChanges) {
+ sendMessage(H.DESTROY_ACTIVITY, token, finishing ? 1 : 0,
+ configChanges);
+ }
+
+ public final void scheduleReceiver(Intent intent, ActivityInfo info,
+ CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
+ boolean sync, int sendingUser, int processState) {
+ updateProcessState(processState, false);
+ ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
+ sync, false, mAppThread.asBinder(), sendingUser);
+ r.info = info;
+ r.compatInfo = compatInfo;
+ sendMessage(H.RECEIVER, r);
+ }
+
+ public final void scheduleCreateBackupAgent(ApplicationInfo app,
+ CompatibilityInfo compatInfo, int backupMode) {
+ CreateBackupAgentData d = new CreateBackupAgentData();
+ d.appInfo = app;
+ d.compatInfo = compatInfo;
+ d.backupMode = backupMode;
+
+ sendMessage(H.CREATE_BACKUP_AGENT, d);
+ }
+
+ public final void scheduleDestroyBackupAgent(ApplicationInfo app,
+ CompatibilityInfo compatInfo) {
+ CreateBackupAgentData d = new CreateBackupAgentData();
+ d.appInfo = app;
+ d.compatInfo = compatInfo;
+
+ sendMessage(H.DESTROY_BACKUP_AGENT, d);
+ }
+
+ public final void scheduleCreateService(IBinder token,
+ ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
+ updateProcessState(processState, false);
+ CreateServiceData s = new CreateServiceData();
+ s.token = token;
+ s.info = info;
+ s.compatInfo = compatInfo;
+
+ sendMessage(H.CREATE_SERVICE, s);
+ }
+
+ public final void scheduleBindService(IBinder token, Intent intent,
+ boolean rebind, int processState) {
+ updateProcessState(processState, false);
+ BindServiceData s = new BindServiceData();
+ s.token = token;
+ s.intent = intent;
+ s.rebind = rebind;
+
+ if (DEBUG_SERVICE)
+ Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="
+ + Binder.getCallingUid() + " pid=" + Binder.getCallingPid());
+ sendMessage(H.BIND_SERVICE, s);
+ }
+
+ public final void scheduleUnbindService(IBinder token, Intent intent) {
+ BindServiceData s = new BindServiceData();
+ s.token = token;
+ s.intent = intent;
+
+ sendMessage(H.UNBIND_SERVICE, s);
+ }
+
+ public final void scheduleServiceArgs(IBinder token, ParceledListSlice args) {
+ List<ServiceStartArgs> list = args.getList();
+
+ for (int i = 0; i < list.size(); i++) {
+ ServiceStartArgs ssa = list.get(i);
+ ServiceArgsData s = new ServiceArgsData();
+ s.token = token;
+ s.taskRemoved = ssa.taskRemoved;
+ s.startId = ssa.startId;
+ s.flags = ssa.flags;
+ s.args = ssa.args;
+
+ sendMessage(H.SERVICE_ARGS, s);
+ }
+ }
+
+ public final void scheduleStopService(IBinder token) {
+ sendMessage(H.STOP_SERVICE, token);
+ }
+
+ public final void bindApplication(String processName, ApplicationInfo appInfo,
+ List<ProviderInfo> providers, ComponentName instrumentationName,
+ ProfilerInfo profilerInfo, Bundle instrumentationArgs,
+ IInstrumentationWatcher instrumentationWatcher,
+ IUiAutomationConnection instrumentationUiConnection, int debugMode,
+ boolean enableBinderTracking, boolean trackAllocation,
+ boolean isRestrictedBackupMode, boolean persistent, Configuration config,
+ CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
+ String buildSerial) {
+
+ if (services != null) {
+ // Setup the service cache in the ServiceManager
+ ServiceManager.initServiceCache(services);
+ }
+
+ setCoreSettings(coreSettings);
+
+ AppBindData data = new AppBindData();
+ data.processName = processName;
+ data.appInfo = appInfo;
+ data.providers = providers;
+ data.instrumentationName = instrumentationName;
+ data.instrumentationArgs = instrumentationArgs;
+ data.instrumentationWatcher = instrumentationWatcher;
+ data.instrumentationUiAutomationConnection = instrumentationUiConnection;
+ data.debugMode = debugMode;
+ data.enableBinderTracking = enableBinderTracking;
+ data.trackAllocation = trackAllocation;
+ data.restrictedBackupMode = isRestrictedBackupMode;
+ data.persistent = persistent;
+ data.config = config;
+ data.compatInfo = compatInfo;
+ data.initProfilerInfo = profilerInfo;
+ data.buildSerial = buildSerial;
+ sendMessage(H.BIND_APPLICATION, data);
+ }
+
+ public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = entryPoint;
+ args.arg2 = entryPointArgs;
+ sendMessage(H.RUN_ISOLATED_ENTRY_POINT, args);
+ }
+
+ public final void scheduleExit() {
+ sendMessage(H.EXIT_APPLICATION, null);
+ }
+
+ public final void scheduleSuicide() {
+ sendMessage(H.SUICIDE, null);
+ }
+
+ public void scheduleConfigurationChanged(Configuration config) {
+ updatePendingConfiguration(config);
+ sendMessage(H.CONFIGURATION_CHANGED, config);
+ }
+
+ public void scheduleApplicationInfoChanged(ApplicationInfo ai) {
+ sendMessage(H.APPLICATION_INFO_CHANGED, ai);
+ }
+
+ public void updateTimeZone() {
+ TimeZone.setDefault(null);
+ }
+
+ public void clearDnsCache() {
+ // a non-standard API to get this to libcore
+ InetAddress.clearDnsCache();
+ // Allow libcore to perform the necessary actions as it sees fit upon a network
+ // configuration change.
+ NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged();
+ }
+
+ public void setHttpProxy(String host, String port, String exclList, Uri pacFileUrl) {
+ final ConnectivityManager cm = ConnectivityManager.from(getSystemContext());
+ final Network network = cm.getBoundNetworkForProcess();
+ if (network != null) {
+ Proxy.setHttpProxySystemProperty(cm.getDefaultProxy());
+ } else {
+ Proxy.setHttpProxySystemProperty(host, port, exclList, pacFileUrl);
+ }
+ }
+
+ public void processInBackground() {
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ mH.sendMessage(mH.obtainMessage(H.GC_WHEN_IDLE));
+ }
+
+ public void dumpService(ParcelFileDescriptor pfd, IBinder servicetoken, String[] args) {
+ DumpComponentInfo data = new DumpComponentInfo();
+ try {
+ data.fd = pfd.dup();
+ data.token = servicetoken;
+ data.args = args;
+ sendMessage(H.DUMP_SERVICE, data, 0, 0, true /*async*/);
+ } catch (IOException e) {
+ Slog.w(TAG, "dumpService failed", e);
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ // This function exists to make sure all receiver dispatching is
+ // correctly ordered, since these are one-way calls and the binder driver
+ // applies transaction ordering per object for such calls.
+ public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
+ int resultCode, String dataStr, Bundle extras, boolean ordered,
+ boolean sticky, int sendingUser, int processState) throws RemoteException {
+ updateProcessState(processState, false);
+ receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
+ sticky, sendingUser);
+ }
+
+ @Override
+ public void scheduleLowMemory() {
+ sendMessage(H.LOW_MEMORY, null);
+ }
+
+ @Override
+ public void scheduleActivityConfigurationChanged(
+ IBinder token, Configuration overrideConfig) {
+ sendMessage(H.ACTIVITY_CONFIGURATION_CHANGED,
+ new ActivityConfigChangeData(token, overrideConfig));
+ }
+
+ @Override
+ public void scheduleActivityMovedToDisplay(IBinder token, int displayId,
+ Configuration overrideConfig) {
+ sendMessage(H.ACTIVITY_MOVED_TO_DISPLAY,
+ new ActivityConfigChangeData(token, overrideConfig), displayId);
+ }
+
+ @Override
+ public void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) {
+ sendMessage(H.PROFILER_CONTROL, profilerInfo, start ? 1 : 0, profileType);
+ }
+
+ @Override
+ public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String path,
+ ParcelFileDescriptor fd) {
+ DumpHeapData dhd = new DumpHeapData();
+ dhd.managed = managed;
+ dhd.mallocInfo = mallocInfo;
+ dhd.runGc = runGc;
+ dhd.path = path;
+ dhd.fd = fd;
+ sendMessage(H.DUMP_HEAP, dhd, 0, 0, true /*async*/);
+ }
+
+ public void attachAgent(String agent) {
+ sendMessage(H.ATTACH_AGENT, agent);
+ }
+
+ public void setSchedulingGroup(int group) {
+ // Note: do this immediately, since going into the foreground
+ // should happen regardless of what pending work we have to do
+ // and the activity manager will wait for us to report back that
+ // we are done before sending us to the background.
+ try {
+ Process.setProcessGroup(Process.myPid(), group);
+ } catch (Exception e) {
+ Slog.w(TAG, "Failed setting process group to " + group, e);
+ }
+ }
+
+ public void dispatchPackageBroadcast(int cmd, String[] packages) {
+ sendMessage(H.DISPATCH_PACKAGE_BROADCAST, packages, cmd);
+ }
+
+ public void scheduleCrash(String msg) {
+ sendMessage(H.SCHEDULE_CRASH, msg);
+ }
+
+ public void dumpActivity(ParcelFileDescriptor pfd, IBinder activitytoken,
+ String prefix, String[] args) {
+ DumpComponentInfo data = new DumpComponentInfo();
+ try {
+ data.fd = pfd.dup();
+ data.token = activitytoken;
+ data.prefix = prefix;
+ data.args = args;
+ sendMessage(H.DUMP_ACTIVITY, data, 0, 0, true /*async*/);
+ } catch (IOException e) {
+ Slog.w(TAG, "dumpActivity failed", e);
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ public void dumpProvider(ParcelFileDescriptor pfd, IBinder providertoken,
+ String[] args) {
+ DumpComponentInfo data = new DumpComponentInfo();
+ try {
+ data.fd = pfd.dup();
+ data.token = providertoken;
+ data.args = args;
+ sendMessage(H.DUMP_PROVIDER, data, 0, 0, true /*async*/);
+ } catch (IOException e) {
+ Slog.w(TAG, "dumpProvider failed", e);
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ @Override
+ public void dumpMemInfo(ParcelFileDescriptor pfd, Debug.MemoryInfo mem, boolean checkin,
+ boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
+ boolean dumpUnreachable, String[] args) {
+ FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor());
+ PrintWriter pw = new FastPrintWriter(fout);
+ try {
+ dumpMemInfo(pw, mem, checkin, dumpFullInfo, dumpDalvik, dumpSummaryOnly, dumpUnreachable);
+ } finally {
+ pw.flush();
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ private void dumpMemInfo(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
+ boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly, boolean dumpUnreachable) {
+ long nativeMax = Debug.getNativeHeapSize() / 1024;
+ long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024;
+ long nativeFree = Debug.getNativeHeapFreeSize() / 1024;
+
+ Runtime runtime = Runtime.getRuntime();
+ runtime.gc(); // Do GC since countInstancesOfClass counts unreachable objects.
+ long dalvikMax = runtime.totalMemory() / 1024;
+ long dalvikFree = runtime.freeMemory() / 1024;
+ long dalvikAllocated = dalvikMax - dalvikFree;
+
+ Class[] classesToCount = new Class[] {
+ ContextImpl.class,
+ Activity.class,
+ WebView.class,
+ OpenSSLSocketImpl.class
+ };
+ long[] instanceCounts = VMDebug.countInstancesOfClasses(classesToCount, true);
+ long appContextInstanceCount = instanceCounts[0];
+ long activityInstanceCount = instanceCounts[1];
+ long webviewInstanceCount = instanceCounts[2];
+ long openSslSocketCount = instanceCounts[3];
+
+ long viewInstanceCount = ViewDebug.getViewInstanceCount();
+ long viewRootInstanceCount = ViewDebug.getViewRootImplCount();
+ int globalAssetCount = AssetManager.getGlobalAssetCount();
+ int globalAssetManagerCount = AssetManager.getGlobalAssetManagerCount();
+ int binderLocalObjectCount = Debug.getBinderLocalObjectCount();
+ int binderProxyObjectCount = Debug.getBinderProxyObjectCount();
+ int binderDeathObjectCount = Debug.getBinderDeathObjectCount();
+ long parcelSize = Parcel.getGlobalAllocSize();
+ long parcelCount = Parcel.getGlobalAllocCount();
+ SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo();
+
+ dumpMemInfoTable(pw, memInfo, checkin, dumpFullInfo, dumpDalvik, dumpSummaryOnly,
+ Process.myPid(),
+ (mBoundApplication != null) ? mBoundApplication.processName : "unknown",
+ nativeMax, nativeAllocated, nativeFree,
+ dalvikMax, dalvikAllocated, dalvikFree);
+
+ if (checkin) {
+ // NOTE: if you change anything significant below, also consider changing
+ // ACTIVITY_THREAD_CHECKIN_VERSION.
+
+ // Object counts
+ pw.print(viewInstanceCount); pw.print(',');
+ pw.print(viewRootInstanceCount); pw.print(',');
+ pw.print(appContextInstanceCount); pw.print(',');
+ pw.print(activityInstanceCount); pw.print(',');
+
+ pw.print(globalAssetCount); pw.print(',');
+ pw.print(globalAssetManagerCount); pw.print(',');
+ pw.print(binderLocalObjectCount); pw.print(',');
+ pw.print(binderProxyObjectCount); pw.print(',');
+
+ pw.print(binderDeathObjectCount); pw.print(',');
+ pw.print(openSslSocketCount); pw.print(',');
+
+ // SQL
+ pw.print(stats.memoryUsed / 1024); pw.print(',');
+ pw.print(stats.memoryUsed / 1024); pw.print(',');
+ pw.print(stats.pageCacheOverflow / 1024); pw.print(',');
+ pw.print(stats.largestMemAlloc / 1024);
+ for (int i = 0; i < stats.dbStats.size(); i++) {
+ DbStats dbStats = stats.dbStats.get(i);
+ pw.print(','); pw.print(dbStats.dbName);
+ pw.print(','); pw.print(dbStats.pageSize);
+ pw.print(','); pw.print(dbStats.dbSize);
+ pw.print(','); pw.print(dbStats.lookaside);
+ pw.print(','); pw.print(dbStats.cache);
+ pw.print(','); pw.print(dbStats.cache);
+ }
+ pw.println();
+
+ return;
+ }
+
+ pw.println(" ");
+ pw.println(" Objects");
+ printRow(pw, TWO_COUNT_COLUMNS, "Views:", viewInstanceCount, "ViewRootImpl:",
+ viewRootInstanceCount);
+
+ printRow(pw, TWO_COUNT_COLUMNS, "AppContexts:", appContextInstanceCount,
+ "Activities:", activityInstanceCount);
+
+ printRow(pw, TWO_COUNT_COLUMNS, "Assets:", globalAssetCount,
+ "AssetManagers:", globalAssetManagerCount);
+
+ printRow(pw, TWO_COUNT_COLUMNS, "Local Binders:", binderLocalObjectCount,
+ "Proxy Binders:", binderProxyObjectCount);
+ printRow(pw, TWO_COUNT_COLUMNS, "Parcel memory:", parcelSize/1024,
+ "Parcel count:", parcelCount);
+ printRow(pw, TWO_COUNT_COLUMNS, "Death Recipients:", binderDeathObjectCount,
+ "OpenSSL Sockets:", openSslSocketCount);
+ printRow(pw, ONE_COUNT_COLUMN, "WebViews:", webviewInstanceCount);
+
+ // SQLite mem info
+ pw.println(" ");
+ pw.println(" SQL");
+ printRow(pw, ONE_COUNT_COLUMN, "MEMORY_USED:", stats.memoryUsed / 1024);
+ printRow(pw, TWO_COUNT_COLUMNS, "PAGECACHE_OVERFLOW:",
+ stats.pageCacheOverflow / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024);
+ pw.println(" ");
+ int N = stats.dbStats.size();
+ if (N > 0) {
+ pw.println(" DATABASES");
+ printRow(pw, " %8s %8s %14s %14s %s", "pgsz", "dbsz", "Lookaside(b)", "cache",
+ "Dbname");
+ for (int i = 0; i < N; i++) {
+ DbStats dbStats = stats.dbStats.get(i);
+ printRow(pw, DB_INFO_FORMAT,
+ (dbStats.pageSize > 0) ? String.valueOf(dbStats.pageSize) : " ",
+ (dbStats.dbSize > 0) ? String.valueOf(dbStats.dbSize) : " ",
+ (dbStats.lookaside > 0) ? String.valueOf(dbStats.lookaside) : " ",
+ dbStats.cache, dbStats.dbName);
+ }
+ }
+
+ // Asset details.
+ String assetAlloc = AssetManager.getAssetAllocations();
+ if (assetAlloc != null) {
+ pw.println(" ");
+ pw.println(" Asset Allocations");
+ pw.print(assetAlloc);
+ }
+
+ // Unreachable native memory
+ if (dumpUnreachable) {
+ boolean showContents = ((mBoundApplication != null)
+ && ((mBoundApplication.appInfo.flags&ApplicationInfo.FLAG_DEBUGGABLE) != 0))
+ || android.os.Build.IS_DEBUGGABLE;
+ pw.println(" ");
+ pw.println(" Unreachable memory");
+ pw.print(Debug.getUnreachableMemory(100, showContents));
+ }
+ }
+
+ @Override
+ public void dumpGfxInfo(ParcelFileDescriptor pfd, String[] args) {
+ nDumpGraphicsInfo(pfd.getFileDescriptor());
+ WindowManagerGlobal.getInstance().dumpGfxInfo(pfd.getFileDescriptor(), args);
+ IoUtils.closeQuietly(pfd);
+ }
+
+ private void dumpDatabaseInfo(ParcelFileDescriptor pfd, String[] args) {
+ PrintWriter pw = new FastPrintWriter(
+ new FileOutputStream(pfd.getFileDescriptor()));
+ PrintWriterPrinter printer = new PrintWriterPrinter(pw);
+ SQLiteDebug.dump(printer, args);
+ pw.flush();
+ }
+
+ @Override
+ public void dumpDbInfo(final ParcelFileDescriptor pfd, final String[] args) {
+ if (mSystemThread) {
+ // Ensure this invocation is asynchronous to prevent writer waiting if buffer cannot
+ // be consumed. But it must duplicate the file descriptor first, since caller might
+ // be closing it.
+ final ParcelFileDescriptor dup;
+ try {
+ dup = pfd.dup();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not dup FD " + pfd.getFileDescriptor().getInt$());
+ return;
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ }
+
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ dumpDatabaseInfo(dup, args);
+ } finally {
+ IoUtils.closeQuietly(dup);
+ }
+ }
+ });
+ } else {
+ dumpDatabaseInfo(pfd, args);
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ @Override
+ public void unstableProviderDied(IBinder provider) {
+ sendMessage(H.UNSTABLE_PROVIDER_DIED, provider);
+ }
+
+ @Override
+ public void requestAssistContextExtras(IBinder activityToken, IBinder requestToken,
+ int requestType, int sessionId, int flags) {
+ RequestAssistContextExtras cmd = new RequestAssistContextExtras();
+ cmd.activityToken = activityToken;
+ cmd.requestToken = requestToken;
+ cmd.requestType = requestType;
+ cmd.sessionId = sessionId;
+ cmd.flags = flags;
+ sendMessage(H.REQUEST_ASSIST_CONTEXT_EXTRAS, cmd);
+ }
+
+ public void setCoreSettings(Bundle coreSettings) {
+ sendMessage(H.SET_CORE_SETTINGS, coreSettings);
+ }
+
+ public void updatePackageCompatibilityInfo(String pkg, CompatibilityInfo info) {
+ UpdateCompatibilityData ucd = new UpdateCompatibilityData();
+ ucd.pkg = pkg;
+ ucd.info = info;
+ sendMessage(H.UPDATE_PACKAGE_COMPATIBILITY_INFO, ucd);
+ }
+
+ public void scheduleTrimMemory(int level) {
+ sendMessage(H.TRIM_MEMORY, null, level);
+ }
+
+ public void scheduleTranslucentConversionComplete(IBinder token, boolean drawComplete) {
+ sendMessage(H.TRANSLUCENT_CONVERSION_COMPLETE, token, drawComplete ? 1 : 0);
+ }
+
+ public void scheduleOnNewActivityOptions(IBinder token, Bundle options) {
+ sendMessage(H.ON_NEW_ACTIVITY_OPTIONS,
+ new Pair<IBinder, ActivityOptions>(token, ActivityOptions.fromBundle(options)));
+ }
+
+ public void setProcessState(int state) {
+ updateProcessState(state, true);
+ }
+
+ public void updateProcessState(int processState, boolean fromIpc) {
+ synchronized (this) {
+ if (mLastProcessState != processState) {
+ mLastProcessState = processState;
+ // Update Dalvik state based on ActivityManager.PROCESS_STATE_* constants.
+ final int DALVIK_PROCESS_STATE_JANK_PERCEPTIBLE = 0;
+ final int DALVIK_PROCESS_STATE_JANK_IMPERCEPTIBLE = 1;
+ int dalvikProcessState = DALVIK_PROCESS_STATE_JANK_IMPERCEPTIBLE;
+ // TODO: Tune this since things like gmail sync are important background but not jank perceptible.
+ if (processState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
+ dalvikProcessState = DALVIK_PROCESS_STATE_JANK_PERCEPTIBLE;
+ }
+ VMRuntime.getRuntime().updateProcessState(dalvikProcessState);
+ if (false) {
+ Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState
+ + (fromIpc ? " (from ipc": ""));
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates {@link #mNetworkBlockSeq}. This is used by ActivityManagerService to inform
+ * the main thread that it needs to wait for the network rules to get updated before
+ * launching an activity.
+ */
+ @Override
+ public void setNetworkBlockSeq(long procStateSeq) {
+ synchronized (mNetworkPolicyLock) {
+ mNetworkBlockSeq = procStateSeq;
+ }
+ }
+
+ @Override
+ public void scheduleInstallProvider(ProviderInfo provider) {
+ sendMessage(H.INSTALL_PROVIDER, provider);
+ }
+
+ @Override
+ public final void updateTimePrefs(int timeFormatPreference) {
+ final Boolean timeFormatPreferenceBool;
+ // For convenience we are using the Intent extra values.
+ if (timeFormatPreference == Intent.EXTRA_TIME_PREF_VALUE_USE_12_HOUR) {
+ timeFormatPreferenceBool = Boolean.FALSE;
+ } else if (timeFormatPreference == Intent.EXTRA_TIME_PREF_VALUE_USE_24_HOUR) {
+ timeFormatPreferenceBool = Boolean.TRUE;
+ } else {
+ // timeFormatPreference == Intent.EXTRA_TIME_PREF_VALUE_USE_LOCALE_DEFAULT
+ // (or unknown).
+ timeFormatPreferenceBool = null;
+ }
+ DateFormat.set24HourTimePref(timeFormatPreferenceBool);
+ }
+
+ @Override
+ public void scheduleEnterAnimationComplete(IBinder token) {
+ sendMessage(H.ENTER_ANIMATION_COMPLETE, token);
+ }
+
+ @Override
+ public void notifyCleartextNetwork(byte[] firstPacket) {
+ if (StrictMode.vmCleartextNetworkEnabled()) {
+ StrictMode.onCleartextNetworkDetected(firstPacket);
+ }
+ }
+
+ @Override
+ public void startBinderTracking() {
+ sendMessage(H.START_BINDER_TRACKING, null);
+ }
+
+ @Override
+ public void stopBinderTrackingAndDump(ParcelFileDescriptor pfd) {
+ try {
+ sendMessage(H.STOP_BINDER_TRACKING_AND_DUMP, pfd.dup());
+ } catch (IOException e) {
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ @Override
+ public void scheduleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode,
+ Configuration overrideConfig) throws RemoteException {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = token;
+ args.arg2 = overrideConfig;
+ args.argi1 = isInMultiWindowMode ? 1 : 0;
+ sendMessage(H.MULTI_WINDOW_MODE_CHANGED, args);
+ }
+
+ @Override
+ public void schedulePictureInPictureModeChanged(IBinder token, boolean isInPipMode,
+ Configuration overrideConfig) throws RemoteException {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = token;
+ args.arg2 = overrideConfig;
+ args.argi1 = isInPipMode ? 1 : 0;
+ sendMessage(H.PICTURE_IN_PICTURE_MODE_CHANGED, args);
+ }
+
+ @Override
+ public void scheduleLocalVoiceInteractionStarted(IBinder token,
+ IVoiceInteractor voiceInteractor) throws RemoteException {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = token;
+ args.arg2 = voiceInteractor;
+ sendMessage(H.LOCAL_VOICE_INTERACTION_STARTED, args);
+ }
+
+ @Override
+ public void handleTrustStorageUpdate() {
+ NetworkSecurityPolicy.getInstance().handleTrustStorageUpdate();
+ }
+ }
+
+ private int getLifecycleSeq() {
+ synchronized (mResourcesManager) {
+ return mLifecycleSeq++;
+ }
+ }
+
+ private class H extends Handler {
+ public static final int LAUNCH_ACTIVITY = 100;
+ public static final int PAUSE_ACTIVITY = 101;
+ public static final int PAUSE_ACTIVITY_FINISHING= 102;
+ public static final int STOP_ACTIVITY_SHOW = 103;
+ public static final int STOP_ACTIVITY_HIDE = 104;
+ public static final int SHOW_WINDOW = 105;
+ public static final int HIDE_WINDOW = 106;
+ public static final int RESUME_ACTIVITY = 107;
+ public static final int SEND_RESULT = 108;
+ public static final int DESTROY_ACTIVITY = 109;
+ public static final int BIND_APPLICATION = 110;
+ public static final int EXIT_APPLICATION = 111;
+ public static final int NEW_INTENT = 112;
+ public static final int RECEIVER = 113;
+ public static final int CREATE_SERVICE = 114;
+ public static final int SERVICE_ARGS = 115;
+ public static final int STOP_SERVICE = 116;
+
+ public static final int CONFIGURATION_CHANGED = 118;
+ public static final int CLEAN_UP_CONTEXT = 119;
+ public static final int GC_WHEN_IDLE = 120;
+ public static final int BIND_SERVICE = 121;
+ public static final int UNBIND_SERVICE = 122;
+ public static final int DUMP_SERVICE = 123;
+ public static final int LOW_MEMORY = 124;
+ public static final int ACTIVITY_CONFIGURATION_CHANGED = 125;
+ public static final int RELAUNCH_ACTIVITY = 126;
+ public static final int PROFILER_CONTROL = 127;
+ public static final int CREATE_BACKUP_AGENT = 128;
+ public static final int DESTROY_BACKUP_AGENT = 129;
+ public static final int SUICIDE = 130;
+ public static final int REMOVE_PROVIDER = 131;
+ public static final int ENABLE_JIT = 132;
+ public static final int DISPATCH_PACKAGE_BROADCAST = 133;
+ public static final int SCHEDULE_CRASH = 134;
+ public static final int DUMP_HEAP = 135;
+ public static final int DUMP_ACTIVITY = 136;
+ public static final int SLEEPING = 137;
+ public static final int SET_CORE_SETTINGS = 138;
+ public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139;
+ public static final int TRIM_MEMORY = 140;
+ public static final int DUMP_PROVIDER = 141;
+ public static final int UNSTABLE_PROVIDER_DIED = 142;
+ public static final int REQUEST_ASSIST_CONTEXT_EXTRAS = 143;
+ public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144;
+ public static final int INSTALL_PROVIDER = 145;
+ public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
+ public static final int ENTER_ANIMATION_COMPLETE = 149;
+ public static final int START_BINDER_TRACKING = 150;
+ public static final int STOP_BINDER_TRACKING_AND_DUMP = 151;
+ public static final int MULTI_WINDOW_MODE_CHANGED = 152;
+ public static final int PICTURE_IN_PICTURE_MODE_CHANGED = 153;
+ public static final int LOCAL_VOICE_INTERACTION_STARTED = 154;
+ public static final int ATTACH_AGENT = 155;
+ public static final int APPLICATION_INFO_CHANGED = 156;
+ public static final int ACTIVITY_MOVED_TO_DISPLAY = 157;
+ public static final int RUN_ISOLATED_ENTRY_POINT = 158;
+
+ String codeToString(int code) {
+ if (DEBUG_MESSAGES) {
+ switch (code) {
+ case LAUNCH_ACTIVITY: return "LAUNCH_ACTIVITY";
+ case PAUSE_ACTIVITY: return "PAUSE_ACTIVITY";
+ case PAUSE_ACTIVITY_FINISHING: return "PAUSE_ACTIVITY_FINISHING";
+ case STOP_ACTIVITY_SHOW: return "STOP_ACTIVITY_SHOW";
+ case STOP_ACTIVITY_HIDE: return "STOP_ACTIVITY_HIDE";
+ case SHOW_WINDOW: return "SHOW_WINDOW";
+ case HIDE_WINDOW: return "HIDE_WINDOW";
+ case RESUME_ACTIVITY: return "RESUME_ACTIVITY";
+ case SEND_RESULT: return "SEND_RESULT";
+ case DESTROY_ACTIVITY: return "DESTROY_ACTIVITY";
+ case BIND_APPLICATION: return "BIND_APPLICATION";
+ case EXIT_APPLICATION: return "EXIT_APPLICATION";
+ case NEW_INTENT: return "NEW_INTENT";
+ case RECEIVER: return "RECEIVER";
+ case CREATE_SERVICE: return "CREATE_SERVICE";
+ case SERVICE_ARGS: return "SERVICE_ARGS";
+ case STOP_SERVICE: return "STOP_SERVICE";
+ case CONFIGURATION_CHANGED: return "CONFIGURATION_CHANGED";
+ case CLEAN_UP_CONTEXT: return "CLEAN_UP_CONTEXT";
+ case GC_WHEN_IDLE: return "GC_WHEN_IDLE";
+ case BIND_SERVICE: return "BIND_SERVICE";
+ case UNBIND_SERVICE: return "UNBIND_SERVICE";
+ case DUMP_SERVICE: return "DUMP_SERVICE";
+ case LOW_MEMORY: return "LOW_MEMORY";
+ case ACTIVITY_CONFIGURATION_CHANGED: return "ACTIVITY_CONFIGURATION_CHANGED";
+ case ACTIVITY_MOVED_TO_DISPLAY: return "ACTIVITY_MOVED_TO_DISPLAY";
+ case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY";
+ case PROFILER_CONTROL: return "PROFILER_CONTROL";
+ case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT";
+ case DESTROY_BACKUP_AGENT: return "DESTROY_BACKUP_AGENT";
+ case SUICIDE: return "SUICIDE";
+ case REMOVE_PROVIDER: return "REMOVE_PROVIDER";
+ case ENABLE_JIT: return "ENABLE_JIT";
+ case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST";
+ case SCHEDULE_CRASH: return "SCHEDULE_CRASH";
+ case DUMP_HEAP: return "DUMP_HEAP";
+ case DUMP_ACTIVITY: return "DUMP_ACTIVITY";
+ case SLEEPING: return "SLEEPING";
+ case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS";
+ case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO";
+ case TRIM_MEMORY: return "TRIM_MEMORY";
+ case DUMP_PROVIDER: return "DUMP_PROVIDER";
+ case UNSTABLE_PROVIDER_DIED: return "UNSTABLE_PROVIDER_DIED";
+ case REQUEST_ASSIST_CONTEXT_EXTRAS: return "REQUEST_ASSIST_CONTEXT_EXTRAS";
+ case TRANSLUCENT_CONVERSION_COMPLETE: return "TRANSLUCENT_CONVERSION_COMPLETE";
+ case INSTALL_PROVIDER: return "INSTALL_PROVIDER";
+ case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS";
+ case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE";
+ case MULTI_WINDOW_MODE_CHANGED: return "MULTI_WINDOW_MODE_CHANGED";
+ case PICTURE_IN_PICTURE_MODE_CHANGED: return "PICTURE_IN_PICTURE_MODE_CHANGED";
+ case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED";
+ case ATTACH_AGENT: return "ATTACH_AGENT";
+ case APPLICATION_INFO_CHANGED: return "APPLICATION_INFO_CHANGED";
+ case RUN_ISOLATED_ENTRY_POINT: return "RUN_ISOLATED_ENTRY_POINT";
+ }
+ }
+ return Integer.toString(code);
+ }
+ public void handleMessage(Message msg) {
+ if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
+ switch (msg.what) {
+ case LAUNCH_ACTIVITY: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
+ final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
+
+ r.packageInfo = getPackageInfoNoCheck(
+ r.activityInfo.applicationInfo, r.compatInfo);
+ handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case RELAUNCH_ACTIVITY: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
+ ActivityClientRecord r = (ActivityClientRecord)msg.obj;
+ handleRelaunchActivity(r);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case PAUSE_ACTIVITY: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
+ SomeArgs args = (SomeArgs) msg.obj;
+ handlePauseActivity((IBinder) args.arg1, false,
+ (args.argi1 & USER_LEAVING) != 0, args.argi2,
+ (args.argi1 & DONT_REPORT) != 0, args.argi3);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case PAUSE_ACTIVITY_FINISHING: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
+ SomeArgs args = (SomeArgs) msg.obj;
+ handlePauseActivity((IBinder) args.arg1, true, (args.argi1 & USER_LEAVING) != 0,
+ args.argi2, (args.argi1 & DONT_REPORT) != 0, args.argi3);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case STOP_ACTIVITY_SHOW: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
+ SomeArgs args = (SomeArgs) msg.obj;
+ handleStopActivity((IBinder) args.arg1, true, args.argi2, args.argi3);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case STOP_ACTIVITY_HIDE: {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
+ SomeArgs args = (SomeArgs) msg.obj;
+ handleStopActivity((IBinder) args.arg1, false, args.argi2, args.argi3);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ } break;
+ case SHOW_WINDOW:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow");
+ handleWindowVisibility((IBinder)msg.obj, true);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case HIDE_WINDOW:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityHideWindow");
+ handleWindowVisibility((IBinder)msg.obj, false);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case RESUME_ACTIVITY:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
+ SomeArgs args = (SomeArgs) msg.obj;
+ handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true,
+ args.argi3, "RESUME_ACTIVITY");
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case SEND_RESULT:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult");
+ handleSendResult((ResultData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case DESTROY_ACTIVITY:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy");
+ handleDestroyActivity((IBinder)msg.obj, msg.arg1 != 0,
+ msg.arg2, false);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case BIND_APPLICATION:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
+ AppBindData data = (AppBindData)msg.obj;
+ handleBindApplication(data);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case EXIT_APPLICATION:
+ if (mInitialApplication != null) {
+ mInitialApplication.onTerminate();
+ }
+ Looper.myLooper().quit();
+ break;
+ case NEW_INTENT:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityNewIntent");
+ handleNewIntent((NewIntentData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case RECEIVER:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp");
+ handleReceiver((ReceiverData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case CREATE_SERVICE:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceCreate: " + String.valueOf(msg.obj)));
+ handleCreateService((CreateServiceData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case BIND_SERVICE:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");
+ handleBindService((BindServiceData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case UNBIND_SERVICE:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceUnbind");
+ handleUnbindService((BindServiceData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case SERVICE_ARGS:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceStart: " + String.valueOf(msg.obj)));
+ handleServiceArgs((ServiceArgsData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case STOP_SERVICE:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceStop");
+ handleStopService((IBinder)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case CONFIGURATION_CHANGED:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged");
+ mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi;
+ mUpdatingSystemConfig = true;
+ try {
+ handleConfigurationChanged((Configuration) msg.obj, null);
+ } finally {
+ mUpdatingSystemConfig = false;
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case CLEAN_UP_CONTEXT:
+ ContextCleanupInfo cci = (ContextCleanupInfo)msg.obj;
+ cci.context.performFinalCleanup(cci.who, cci.what);
+ break;
+ case GC_WHEN_IDLE:
+ scheduleGcIdler();
+ break;
+ case DUMP_SERVICE:
+ handleDumpService((DumpComponentInfo)msg.obj);
+ break;
+ case LOW_MEMORY:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "lowMemory");
+ handleLowMemory();
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case ACTIVITY_CONFIGURATION_CHANGED:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged");
+ handleActivityConfigurationChanged((ActivityConfigChangeData) msg.obj,
+ INVALID_DISPLAY);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case ACTIVITY_MOVED_TO_DISPLAY:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay");
+ handleActivityConfigurationChanged((ActivityConfigChangeData) msg.obj,
+ msg.arg1 /* displayId */);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case PROFILER_CONTROL:
+ handleProfilerControl(msg.arg1 != 0, (ProfilerInfo)msg.obj, msg.arg2);
+ break;
+ case CREATE_BACKUP_AGENT:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupCreateAgent");
+ handleCreateBackupAgent((CreateBackupAgentData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case DESTROY_BACKUP_AGENT:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupDestroyAgent");
+ handleDestroyBackupAgent((CreateBackupAgentData)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case SUICIDE:
+ Process.killProcess(Process.myPid());
+ break;
+ case REMOVE_PROVIDER:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "providerRemove");
+ completeRemoveProvider((ProviderRefCount)msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case ENABLE_JIT:
+ ensureJitEnabled();
+ break;
+ case DISPATCH_PACKAGE_BROADCAST:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastPackage");
+ handleDispatchPackageBroadcast(msg.arg1, (String[])msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case SCHEDULE_CRASH:
+ throw new RemoteServiceException((String)msg.obj);
+ case DUMP_HEAP:
+ handleDumpHeap((DumpHeapData) msg.obj);
+ break;
+ case DUMP_ACTIVITY:
+ handleDumpActivity((DumpComponentInfo)msg.obj);
+ break;
+ case DUMP_PROVIDER:
+ handleDumpProvider((DumpComponentInfo)msg.obj);
+ break;
+ case SLEEPING:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "sleeping");
+ handleSleeping((IBinder)msg.obj, msg.arg1 != 0);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case SET_CORE_SETTINGS:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setCoreSettings");
+ handleSetCoreSettings((Bundle) msg.obj);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case UPDATE_PACKAGE_COMPATIBILITY_INFO:
+ handleUpdatePackageCompatibilityInfo((UpdateCompatibilityData)msg.obj);
+ break;
+ case TRIM_MEMORY:
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "trimMemory");
+ handleTrimMemory(msg.arg1);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
+ case UNSTABLE_PROVIDER_DIED:
+ handleUnstableProviderDied((IBinder)msg.obj, false);
+ break;
+ case REQUEST_ASSIST_CONTEXT_EXTRAS:
+ handleRequestAssistContextExtras((RequestAssistContextExtras)msg.obj);
+ break;
+ case TRANSLUCENT_CONVERSION_COMPLETE:
+ handleTranslucentConversionComplete((IBinder)msg.obj, msg.arg1 == 1);
+ break;
+ case INSTALL_PROVIDER:
+ handleInstallProvider((ProviderInfo) msg.obj);
+ break;
+ case ON_NEW_ACTIVITY_OPTIONS:
+ Pair<IBinder, ActivityOptions> pair = (Pair<IBinder, ActivityOptions>) msg.obj;
+ onNewActivityOptions(pair.first, pair.second);
+ break;
+ case ENTER_ANIMATION_COMPLETE:
+ handleEnterAnimationComplete((IBinder) msg.obj);
+ break;
+ case START_BINDER_TRACKING:
+ handleStartBinderTracking();
+ break;
+ case STOP_BINDER_TRACKING_AND_DUMP:
+ handleStopBinderTrackingAndDump((ParcelFileDescriptor) msg.obj);
+ break;
+ case MULTI_WINDOW_MODE_CHANGED:
+ handleMultiWindowModeChanged((IBinder) ((SomeArgs) msg.obj).arg1,
+ ((SomeArgs) msg.obj).argi1 == 1,
+ (Configuration) ((SomeArgs) msg.obj).arg2);
+ break;
+ case PICTURE_IN_PICTURE_MODE_CHANGED:
+ handlePictureInPictureModeChanged((IBinder) ((SomeArgs) msg.obj).arg1,
+ ((SomeArgs) msg.obj).argi1 == 1,
+ (Configuration) ((SomeArgs) msg.obj).arg2);
+ break;
+ case LOCAL_VOICE_INTERACTION_STARTED:
+ handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
+ (IVoiceInteractor) ((SomeArgs) msg.obj).arg2);
+ break;
+ case ATTACH_AGENT:
+ handleAttachAgent((String) msg.obj);
+ break;
+ case APPLICATION_INFO_CHANGED:
+ mUpdatingSystemConfig = true;
+ try {
+ handleApplicationInfoChanged((ApplicationInfo) msg.obj);
+ } finally {
+ mUpdatingSystemConfig = false;
+ }
+ break;
+ case RUN_ISOLATED_ENTRY_POINT:
+ handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1,
+ (String[]) ((SomeArgs) msg.obj).arg2);
+ break;
+ }
+ Object obj = msg.obj;
+ if (obj instanceof SomeArgs) {
+ ((SomeArgs) obj).recycle();
+ }
+ if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
+ }
+ }
+
+ private class Idler implements MessageQueue.IdleHandler {
+ @Override
+ public final boolean queueIdle() {
+ ActivityClientRecord a = mNewActivities;
+ boolean stopProfiling = false;
+ if (mBoundApplication != null && mProfiler.profileFd != null
+ && mProfiler.autoStopProfiler) {
+ stopProfiling = true;
+ }
+ if (a != null) {
+ mNewActivities = null;
+ IActivityManager am = ActivityManager.getService();
+ ActivityClientRecord prev;
+ do {
+ if (localLOGV) Slog.v(
+ TAG, "Reporting idle of " + a +
+ " finished=" +
+ (a.activity != null && a.activity.mFinished));
+ if (a.activity != null && !a.activity.mFinished) {
+ try {
+ am.activityIdle(a.token, a.createdConfig, stopProfiling);
+ a.createdConfig = null;
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ prev = a;
+ a = a.nextIdle;
+ prev.nextIdle = null;
+ } while (a != null);
+ }
+ if (stopProfiling) {
+ mProfiler.stopProfiling();
+ }
+ ensureJitEnabled();
+ return false;
+ }
+ }
+
+ final class GcIdler implements MessageQueue.IdleHandler {
+ @Override
+ public final boolean queueIdle() {
+ doGcIfNeeded();
+ return false;
+ }
+ }
+
+ public static ActivityThread currentActivityThread() {
+ return sCurrentActivityThread;
+ }
+
+ public static boolean isSystem() {
+ return (sCurrentActivityThread != null) ? sCurrentActivityThread.mSystemThread : false;
+ }
+
+ public static String currentOpPackageName() {
+ ActivityThread am = currentActivityThread();
+ return (am != null && am.getApplication() != null)
+ ? am.getApplication().getOpPackageName() : null;
+ }
+
+ public static String currentPackageName() {
+ ActivityThread am = currentActivityThread();
+ return (am != null && am.mBoundApplication != null)
+ ? am.mBoundApplication.appInfo.packageName : null;
+ }
+
+ public static String currentProcessName() {
+ ActivityThread am = currentActivityThread();
+ return (am != null && am.mBoundApplication != null)
+ ? am.mBoundApplication.processName : null;
+ }
+
+ public static Application currentApplication() {
+ ActivityThread am = currentActivityThread();
+ return am != null ? am.mInitialApplication : null;
+ }
+
+ public static IPackageManager getPackageManager() {
+ if (sPackageManager != null) {
+ //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
+ return sPackageManager;
+ }
+ IBinder b = ServiceManager.getService("package");
+ //Slog.v("PackageManager", "default service binder = " + b);
+ sPackageManager = IPackageManager.Stub.asInterface(b);
+ //Slog.v("PackageManager", "default service = " + sPackageManager);
+ return sPackageManager;
+ }
+
+ private Configuration mMainThreadConfig = new Configuration();
+
+ Configuration applyConfigCompatMainThread(int displayDensity, Configuration config,
+ CompatibilityInfo compat) {
+ if (config == null) {
+ return null;
+ }
+ if (!compat.supportsScreen()) {
+ mMainThreadConfig.setTo(config);
+ config = mMainThreadConfig;
+ compat.applyToConfiguration(displayDensity, config);
+ }
+ return config;
+ }
+
+ /**
+ * Creates the top level resources for the given package. Will return an existing
+ * Resources if one has already been created.
+ */
+ Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
+ String[] libDirs, int displayId, LoadedApk pkgInfo) {
+ return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
+ displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
+ }
+
+ final Handler getHandler() {
+ return mH;
+ }
+
+ public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
+ int flags) {
+ return getPackageInfo(packageName, compatInfo, flags, UserHandle.myUserId());
+ }
+
+ public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
+ int flags, int userId) {
+ final boolean differentUser = (UserHandle.myUserId() != userId);
+ synchronized (mResourcesManager) {
+ WeakReference<LoadedApk> ref;
+ if (differentUser) {
+ // Caching not supported across users
+ ref = null;
+ } else if ((flags & Context.CONTEXT_INCLUDE_CODE) != 0) {
+ ref = mPackages.get(packageName);
+ } else {
+ ref = mResourcePackages.get(packageName);
+ }
+
+ LoadedApk packageInfo = ref != null ? ref.get() : null;
+ //Slog.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo);
+ //if (packageInfo != null) Slog.i(TAG, "isUptoDate " + packageInfo.mResDir
+ // + ": " + packageInfo.mResources.getAssets().isUpToDate());
+ if (packageInfo != null && (packageInfo.mResources == null
+ || packageInfo.mResources.getAssets().isUpToDate())) {
+ if (packageInfo.isSecurityViolation()
+ && (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) {
+ throw new SecurityException(
+ "Requesting code from " + packageName
+ + " to be run in process "
+ + mBoundApplication.processName
+ + "/" + mBoundApplication.appInfo.uid);
+ }
+ return packageInfo;
+ }
+ }
+
+ ApplicationInfo ai = null;
+ try {
+ ai = getPackageManager().getApplicationInfo(packageName,
+ PackageManager.GET_SHARED_LIBRARY_FILES
+ | PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ if (ai != null) {
+ return getPackageInfo(ai, compatInfo, flags);
+ }
+
+ return null;
+ }
+
+ public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
+ int flags) {
+ boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0;
+ boolean securityViolation = includeCode && ai.uid != 0
+ && ai.uid != Process.SYSTEM_UID && (mBoundApplication != null
+ ? !UserHandle.isSameApp(ai.uid, mBoundApplication.appInfo.uid)
+ : true);
+ boolean registerPackage = includeCode && (flags&Context.CONTEXT_REGISTER_PACKAGE) != 0;
+ if ((flags&(Context.CONTEXT_INCLUDE_CODE
+ |Context.CONTEXT_IGNORE_SECURITY))
+ == Context.CONTEXT_INCLUDE_CODE) {
+ if (securityViolation) {
+ String msg = "Requesting code from " + ai.packageName
+ + " (with uid " + ai.uid + ")";
+ if (mBoundApplication != null) {
+ msg = msg + " to be run in process "
+ + mBoundApplication.processName + " (with uid "
+ + mBoundApplication.appInfo.uid + ")";
+ }
+ throw new SecurityException(msg);
+ }
+ }
+ return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode,
+ registerPackage);
+ }
+
+ public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
+ CompatibilityInfo compatInfo) {
+ return getPackageInfo(ai, compatInfo, null, false, true, false);
+ }
+
+ public final LoadedApk peekPackageInfo(String packageName, boolean includeCode) {
+ synchronized (mResourcesManager) {
+ WeakReference<LoadedApk> ref;
+ if (includeCode) {
+ ref = mPackages.get(packageName);
+ } else {
+ ref = mResourcePackages.get(packageName);
+ }
+ return ref != null ? ref.get() : null;
+ }
+ }
+
+ private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
+ ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
+ boolean registerPackage) {
+ final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
+ synchronized (mResourcesManager) {
+ WeakReference<LoadedApk> ref;
+ if (differentUser) {
+ // Caching not supported across users
+ ref = null;
+ } else if (includeCode) {
+ ref = mPackages.get(aInfo.packageName);
+ } else {
+ ref = mResourcePackages.get(aInfo.packageName);
+ }
+
+ LoadedApk packageInfo = ref != null ? ref.get() : null;
+ if (packageInfo == null || (packageInfo.mResources != null
+ && !packageInfo.mResources.getAssets().isUpToDate())) {
+ if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
+ : "Loading resource-only package ") + aInfo.packageName
+ + " (in " + (mBoundApplication != null
+ ? mBoundApplication.processName : null)
+ + ")");
+ packageInfo =
+ new LoadedApk(this, aInfo, compatInfo, baseLoader,
+ securityViolation, includeCode &&
+ (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
+
+ if (mSystemThread && "android".equals(aInfo.packageName)) {
+ packageInfo.installSystemApplicationInfo(aInfo,
+ getSystemContext().mPackageInfo.getClassLoader());
+ }
+
+ if (differentUser) {
+ // Caching not supported across users
+ } else if (includeCode) {
+ mPackages.put(aInfo.packageName,
+ new WeakReference<LoadedApk>(packageInfo));
+ } else {
+ mResourcePackages.put(aInfo.packageName,
+ new WeakReference<LoadedApk>(packageInfo));
+ }
+ }
+ return packageInfo;
+ }
+ }
+
+ ActivityThread() {
+ mResourcesManager = ResourcesManager.getInstance();
+ }
+
+ public ApplicationThread getApplicationThread()
+ {
+ return mAppThread;
+ }
+
+ public Instrumentation getInstrumentation()
+ {
+ return mInstrumentation;
+ }
+
+ public boolean isProfiling() {
+ return mProfiler != null && mProfiler.profileFile != null
+ && mProfiler.profileFd == null;
+ }
+
+ public String getProfileFilePath() {
+ return mProfiler.profileFile;
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ public Application getApplication() {
+ return mInitialApplication;
+ }
+
+ public String getProcessName() {
+ return mBoundApplication.processName;
+ }
+
+ public ContextImpl getSystemContext() {
+ synchronized (this) {
+ if (mSystemContext == null) {
+ mSystemContext = ContextImpl.createSystemContext(this);
+ }
+ return mSystemContext;
+ }
+ }
+
+ public ContextImpl getSystemUiContext() {
+ synchronized (this) {
+ if (mSystemUiContext == null) {
+ mSystemUiContext = ContextImpl.createSystemUiContext(getSystemContext());
+ }
+ return mSystemUiContext;
+ }
+ }
+
+ public void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
+ synchronized (this) {
+ getSystemContext().installSystemApplicationInfo(info, classLoader);
+ getSystemUiContext().installSystemApplicationInfo(info, classLoader);
+
+ // give ourselves a default profiler
+ mProfiler = new Profiler();
+ }
+ }
+
+ void ensureJitEnabled() {
+ if (!mJitEnabled) {
+ mJitEnabled = true;
+ dalvik.system.VMRuntime.getRuntime().startJitCompilation();
+ }
+ }
+
+ void scheduleGcIdler() {
+ if (!mGcIdlerScheduled) {
+ mGcIdlerScheduled = true;
+ Looper.myQueue().addIdleHandler(mGcIdler);
+ }
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ }
+
+ void unscheduleGcIdler() {
+ if (mGcIdlerScheduled) {
+ mGcIdlerScheduled = false;
+ Looper.myQueue().removeIdleHandler(mGcIdler);
+ }
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ }
+
+ void doGcIfNeeded() {
+ mGcIdlerScheduled = false;
+ final long now = SystemClock.uptimeMillis();
+ //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
+ // + "m now=" + now);
+ if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
+ //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
+ BinderInternal.forceGc("bg");
+ }
+ }
+
+ private static final String HEAP_FULL_COLUMN
+ = "%13s %8s %8s %8s %8s %8s %8s %8s %8s %8s %8s";
+ private static final String HEAP_COLUMN
+ = "%13s %8s %8s %8s %8s %8s %8s %8s";
+ private static final String ONE_COUNT_COLUMN = "%21s %8d";
+ private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d";
+ private static final String ONE_COUNT_COLUMN_HEADER = "%21s %8s";
+
+ // Formatting for checkin service - update version if row format changes
+ private static final int ACTIVITY_THREAD_CHECKIN_VERSION = 4;
+
+ static void printRow(PrintWriter pw, String format, Object...objs) {
+ pw.println(String.format(format, objs));
+ }
+
+ public static void dumpMemInfoTable(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
+ boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
+ int pid, String processName,
+ long nativeMax, long nativeAllocated, long nativeFree,
+ long dalvikMax, long dalvikAllocated, long dalvikFree) {
+
+ // For checkin, we print one long comma-separated list of values
+ if (checkin) {
+ // NOTE: if you change anything significant below, also consider changing
+ // ACTIVITY_THREAD_CHECKIN_VERSION.
+
+ // Header
+ pw.print(ACTIVITY_THREAD_CHECKIN_VERSION); pw.print(',');
+ pw.print(pid); pw.print(',');
+ pw.print(processName); pw.print(',');
+
+ // Heap info - max
+ pw.print(nativeMax); pw.print(',');
+ pw.print(dalvikMax); pw.print(',');
+ pw.print("N/A,");
+ pw.print(nativeMax + dalvikMax); pw.print(',');
+
+ // Heap info - allocated
+ pw.print(nativeAllocated); pw.print(',');
+ pw.print(dalvikAllocated); pw.print(',');
+ pw.print("N/A,");
+ pw.print(nativeAllocated + dalvikAllocated); pw.print(',');
+
+ // Heap info - free
+ pw.print(nativeFree); pw.print(',');
+ pw.print(dalvikFree); pw.print(',');
+ pw.print("N/A,");
+ pw.print(nativeFree + dalvikFree); pw.print(',');
+
+ // Heap info - proportional set size
+ pw.print(memInfo.nativePss); pw.print(',');
+ pw.print(memInfo.dalvikPss); pw.print(',');
+ pw.print(memInfo.otherPss); pw.print(',');
+ pw.print(memInfo.getTotalPss()); pw.print(',');
+
+ // Heap info - swappable set size
+ pw.print(memInfo.nativeSwappablePss); pw.print(',');
+ pw.print(memInfo.dalvikSwappablePss); pw.print(',');
+ pw.print(memInfo.otherSwappablePss); pw.print(',');
+ pw.print(memInfo.getTotalSwappablePss()); pw.print(',');
+
+ // Heap info - shared dirty
+ pw.print(memInfo.nativeSharedDirty); pw.print(',');
+ pw.print(memInfo.dalvikSharedDirty); pw.print(',');
+ pw.print(memInfo.otherSharedDirty); pw.print(',');
+ pw.print(memInfo.getTotalSharedDirty()); pw.print(',');
+
+ // Heap info - shared clean
+ pw.print(memInfo.nativeSharedClean); pw.print(',');
+ pw.print(memInfo.dalvikSharedClean); pw.print(',');
+ pw.print(memInfo.otherSharedClean); pw.print(',');
+ pw.print(memInfo.getTotalSharedClean()); pw.print(',');
+
+ // Heap info - private Dirty
+ pw.print(memInfo.nativePrivateDirty); pw.print(',');
+ pw.print(memInfo.dalvikPrivateDirty); pw.print(',');
+ pw.print(memInfo.otherPrivateDirty); pw.print(',');
+ pw.print(memInfo.getTotalPrivateDirty()); pw.print(',');
+
+ // Heap info - private Clean
+ pw.print(memInfo.nativePrivateClean); pw.print(',');
+ pw.print(memInfo.dalvikPrivateClean); pw.print(',');
+ pw.print(memInfo.otherPrivateClean); pw.print(',');
+ pw.print(memInfo.getTotalPrivateClean()); pw.print(',');
+
+ // Heap info - swapped out
+ pw.print(memInfo.nativeSwappedOut); pw.print(',');
+ pw.print(memInfo.dalvikSwappedOut); pw.print(',');
+ pw.print(memInfo.otherSwappedOut); pw.print(',');
+ pw.print(memInfo.getTotalSwappedOut()); pw.print(',');
+
+ // Heap info - swapped out pss
+ if (memInfo.hasSwappedOutPss) {
+ pw.print(memInfo.nativeSwappedOutPss); pw.print(',');
+ pw.print(memInfo.dalvikSwappedOutPss); pw.print(',');
+ pw.print(memInfo.otherSwappedOutPss); pw.print(',');
+ pw.print(memInfo.getTotalSwappedOutPss()); pw.print(',');
+ } else {
+ pw.print("N/A,");
+ pw.print("N/A,");
+ pw.print("N/A,");
+ pw.print("N/A,");
+ }
+
+ // Heap info - other areas
+ for (int i=0; i<Debug.MemoryInfo.NUM_OTHER_STATS; i++) {
+ pw.print(Debug.MemoryInfo.getOtherLabel(i)); pw.print(',');
+ pw.print(memInfo.getOtherPss(i)); pw.print(',');
+ pw.print(memInfo.getOtherSwappablePss(i)); pw.print(',');
+ pw.print(memInfo.getOtherSharedDirty(i)); pw.print(',');
+ pw.print(memInfo.getOtherSharedClean(i)); pw.print(',');
+ pw.print(memInfo.getOtherPrivateDirty(i)); pw.print(',');
+ pw.print(memInfo.getOtherPrivateClean(i)); pw.print(',');
+ pw.print(memInfo.getOtherSwappedOut(i)); pw.print(',');
+ if (memInfo.hasSwappedOutPss) {
+ pw.print(memInfo.getOtherSwappedOutPss(i)); pw.print(',');
+ } else {
+ pw.print("N/A,");
+ }
+ }
+ return;
+ }
+
+ if (!dumpSummaryOnly) {
+ if (dumpFullInfo) {
+ printRow(pw, HEAP_FULL_COLUMN, "", "Pss", "Pss", "Shared", "Private",
+ "Shared", "Private", memInfo.hasSwappedOutPss ? "SwapPss" : "Swap",
+ "Heap", "Heap", "Heap");
+ printRow(pw, HEAP_FULL_COLUMN, "", "Total", "Clean", "Dirty", "Dirty",
+ "Clean", "Clean", "Dirty",
+ "Size", "Alloc", "Free");
+ printRow(pw, HEAP_FULL_COLUMN, "", "------", "------", "------", "------",
+ "------", "------", "------", "------", "------", "------");
+ printRow(pw, HEAP_FULL_COLUMN, "Native Heap", memInfo.nativePss,
+ memInfo.nativeSwappablePss, memInfo.nativeSharedDirty,
+ memInfo.nativePrivateDirty, memInfo.nativeSharedClean,
+ memInfo.nativePrivateClean, memInfo.hasSwappedOutPss ?
+ memInfo.nativeSwappedOutPss : memInfo.nativeSwappedOut,
+ nativeMax, nativeAllocated, nativeFree);
+ printRow(pw, HEAP_FULL_COLUMN, "Dalvik Heap", memInfo.dalvikPss,
+ memInfo.dalvikSwappablePss, memInfo.dalvikSharedDirty,
+ memInfo.dalvikPrivateDirty, memInfo.dalvikSharedClean,
+ memInfo.dalvikPrivateClean, memInfo.hasSwappedOutPss ?
+ memInfo.dalvikSwappedOutPss : memInfo.dalvikSwappedOut,
+ dalvikMax, dalvikAllocated, dalvikFree);
+ } else {
+ printRow(pw, HEAP_COLUMN, "", "Pss", "Private",
+ "Private", memInfo.hasSwappedOutPss ? "SwapPss" : "Swap",
+ "Heap", "Heap", "Heap");
+ printRow(pw, HEAP_COLUMN, "", "Total", "Dirty",
+ "Clean", "Dirty", "Size", "Alloc", "Free");
+ printRow(pw, HEAP_COLUMN, "", "------", "------", "------",
+ "------", "------", "------", "------", "------");
+ printRow(pw, HEAP_COLUMN, "Native Heap", memInfo.nativePss,
+ memInfo.nativePrivateDirty,
+ memInfo.nativePrivateClean,
+ memInfo.hasSwappedOutPss ? memInfo.nativeSwappedOutPss :
+ memInfo.nativeSwappedOut,
+ nativeMax, nativeAllocated, nativeFree);
+ printRow(pw, HEAP_COLUMN, "Dalvik Heap", memInfo.dalvikPss,
+ memInfo.dalvikPrivateDirty,
+ memInfo.dalvikPrivateClean,
+ memInfo.hasSwappedOutPss ? memInfo.dalvikSwappedOutPss :
+ memInfo.dalvikSwappedOut,
+ dalvikMax, dalvikAllocated, dalvikFree);
+ }
+
+ int otherPss = memInfo.otherPss;
+ int otherSwappablePss = memInfo.otherSwappablePss;
+ int otherSharedDirty = memInfo.otherSharedDirty;
+ int otherPrivateDirty = memInfo.otherPrivateDirty;
+ int otherSharedClean = memInfo.otherSharedClean;
+ int otherPrivateClean = memInfo.otherPrivateClean;
+ int otherSwappedOut = memInfo.otherSwappedOut;
+ int otherSwappedOutPss = memInfo.otherSwappedOutPss;
+
+ for (int i=0; i<Debug.MemoryInfo.NUM_OTHER_STATS; i++) {
+ final int myPss = memInfo.getOtherPss(i);
+ final int mySwappablePss = memInfo.getOtherSwappablePss(i);
+ final int mySharedDirty = memInfo.getOtherSharedDirty(i);
+ final int myPrivateDirty = memInfo.getOtherPrivateDirty(i);
+ final int mySharedClean = memInfo.getOtherSharedClean(i);
+ final int myPrivateClean = memInfo.getOtherPrivateClean(i);
+ final int mySwappedOut = memInfo.getOtherSwappedOut(i);
+ final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i);
+ if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0
+ || mySharedClean != 0 || myPrivateClean != 0
+ || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) {
+ if (dumpFullInfo) {
+ printRow(pw, HEAP_FULL_COLUMN, Debug.MemoryInfo.getOtherLabel(i),
+ myPss, mySwappablePss, mySharedDirty, myPrivateDirty,
+ mySharedClean, myPrivateClean,
+ memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut,
+ "", "", "");
+ } else {
+ printRow(pw, HEAP_COLUMN, Debug.MemoryInfo.getOtherLabel(i),
+ myPss, myPrivateDirty,
+ myPrivateClean,
+ memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut,
+ "", "", "");
+ }
+ otherPss -= myPss;
+ otherSwappablePss -= mySwappablePss;
+ otherSharedDirty -= mySharedDirty;
+ otherPrivateDirty -= myPrivateDirty;
+ otherSharedClean -= mySharedClean;
+ otherPrivateClean -= myPrivateClean;
+ otherSwappedOut -= mySwappedOut;
+ otherSwappedOutPss -= mySwappedOutPss;
+ }
+ }
+
+ if (dumpFullInfo) {
+ printRow(pw, HEAP_FULL_COLUMN, "Unknown", otherPss, otherSwappablePss,
+ otherSharedDirty, otherPrivateDirty, otherSharedClean, otherPrivateClean,
+ memInfo.hasSwappedOutPss ? otherSwappedOutPss : otherSwappedOut,
+ "", "", "");
+ printRow(pw, HEAP_FULL_COLUMN, "TOTAL", memInfo.getTotalPss(),
+ memInfo.getTotalSwappablePss(),
+ memInfo.getTotalSharedDirty(), memInfo.getTotalPrivateDirty(),
+ memInfo.getTotalSharedClean(), memInfo.getTotalPrivateClean(),
+ memInfo.hasSwappedOutPss ? memInfo.getTotalSwappedOutPss() :
+ memInfo.getTotalSwappedOut(),
+ nativeMax+dalvikMax, nativeAllocated+dalvikAllocated,
+ nativeFree+dalvikFree);
+ } else {
+ printRow(pw, HEAP_COLUMN, "Unknown", otherPss,
+ otherPrivateDirty, otherPrivateClean,
+ memInfo.hasSwappedOutPss ? otherSwappedOutPss : otherSwappedOut,
+ "", "", "");
+ printRow(pw, HEAP_COLUMN, "TOTAL", memInfo.getTotalPss(),
+ memInfo.getTotalPrivateDirty(),
+ memInfo.getTotalPrivateClean(),
+ memInfo.hasSwappedOutPss ? memInfo.getTotalSwappedOutPss() :
+ memInfo.getTotalSwappedOut(),
+ nativeMax+dalvikMax,
+ nativeAllocated+dalvikAllocated, nativeFree+dalvikFree);
+ }
+
+ if (dumpDalvik) {
+ pw.println(" ");
+ pw.println(" Dalvik Details");
+
+ for (int i=Debug.MemoryInfo.NUM_OTHER_STATS;
+ i<Debug.MemoryInfo.NUM_OTHER_STATS + Debug.MemoryInfo.NUM_DVK_STATS; i++) {
+ final int myPss = memInfo.getOtherPss(i);
+ final int mySwappablePss = memInfo.getOtherSwappablePss(i);
+ final int mySharedDirty = memInfo.getOtherSharedDirty(i);
+ final int myPrivateDirty = memInfo.getOtherPrivateDirty(i);
+ final int mySharedClean = memInfo.getOtherSharedClean(i);
+ final int myPrivateClean = memInfo.getOtherPrivateClean(i);
+ final int mySwappedOut = memInfo.getOtherSwappedOut(i);
+ final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i);
+ if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0
+ || mySharedClean != 0 || myPrivateClean != 0
+ || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) {
+ if (dumpFullInfo) {
+ printRow(pw, HEAP_FULL_COLUMN, Debug.MemoryInfo.getOtherLabel(i),
+ myPss, mySwappablePss, mySharedDirty, myPrivateDirty,
+ mySharedClean, myPrivateClean,
+ memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut,
+ "", "", "");
+ } else {
+ printRow(pw, HEAP_COLUMN, Debug.MemoryInfo.getOtherLabel(i),
+ myPss, myPrivateDirty,
+ myPrivateClean,
+ memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut,
+ "", "", "");
+ }
+ }
+ }
+ }
+ }
+
+ pw.println(" ");
+ pw.println(" App Summary");
+ printRow(pw, ONE_COUNT_COLUMN_HEADER, "", "Pss(KB)");
+ printRow(pw, ONE_COUNT_COLUMN_HEADER, "", "------");
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Java Heap:", memInfo.getSummaryJavaHeap());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Native Heap:", memInfo.getSummaryNativeHeap());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Code:", memInfo.getSummaryCode());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Stack:", memInfo.getSummaryStack());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Graphics:", memInfo.getSummaryGraphics());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "Private Other:", memInfo.getSummaryPrivateOther());
+ printRow(pw, ONE_COUNT_COLUMN,
+ "System:", memInfo.getSummarySystem());
+ pw.println(" ");
+ if (memInfo.hasSwappedOutPss) {
+ printRow(pw, TWO_COUNT_COLUMNS,
+ "TOTAL:", memInfo.getSummaryTotalPss(),
+ "TOTAL SWAP PSS:", memInfo.getSummaryTotalSwapPss());
+ } else {
+ printRow(pw, TWO_COUNT_COLUMNS,
+ "TOTAL:", memInfo.getSummaryTotalPss(),
+ "TOTAL SWAP (KB):", memInfo.getSummaryTotalSwap());
+ }
+ }
+
+ public void registerOnActivityPausedListener(Activity activity,
+ OnActivityPausedListener listener) {
+ synchronized (mOnPauseListeners) {
+ ArrayList<OnActivityPausedListener> list = mOnPauseListeners.get(activity);
+ if (list == null) {
+ list = new ArrayList<OnActivityPausedListener>();
+ mOnPauseListeners.put(activity, list);
+ }
+ list.add(listener);
+ }
+ }
+
+ public void unregisterOnActivityPausedListener(Activity activity,
+ OnActivityPausedListener listener) {
+ synchronized (mOnPauseListeners) {
+ ArrayList<OnActivityPausedListener> list = mOnPauseListeners.get(activity);
+ if (list != null) {
+ list.remove(listener);
+ }
+ }
+ }
+
+ public final ActivityInfo resolveActivityInfo(Intent intent) {
+ ActivityInfo aInfo = intent.resolveActivityInfo(
+ mInitialApplication.getPackageManager(), PackageManager.GET_SHARED_LIBRARY_FILES);
+ if (aInfo == null) {
+ // Throw an exception.
+ Instrumentation.checkStartActivityResult(
+ ActivityManager.START_CLASS_NOT_FOUND, intent);
+ }
+ return aInfo;
+ }
+
+ public final Activity startActivityNow(Activity parent, String id,
+ Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,
+ Activity.NonConfigurationInstances lastNonConfigurationInstances) {
+ ActivityClientRecord r = new ActivityClientRecord();
+ r.token = token;
+ r.ident = 0;
+ r.intent = intent;
+ r.state = state;
+ r.parent = parent;
+ r.embeddedID = id;
+ r.activityInfo = activityInfo;
+ r.lastNonConfigurationInstances = lastNonConfigurationInstances;
+ if (localLOGV) {
+ ComponentName compname = intent.getComponent();
+ String name;
+ if (compname != null) {
+ name = compname.toShortString();
+ } else {
+ name = "(Intent " + intent + ").getComponent() returned null";
+ }
+ Slog.v(TAG, "Performing launch: action=" + intent.getAction()
+ + ", comp=" + name
+ + ", token=" + token);
+ }
+ return performLaunchActivity(r, null);
+ }
+
+ public final Activity getActivity(IBinder token) {
+ return mActivities.get(token).activity;
+ }
+
+ public final void sendActivityResult(
+ IBinder token, String id, int requestCode,
+ int resultCode, Intent data) {
+ if (DEBUG_RESULTS) Slog.v(TAG, "sendActivityResult: id=" + id
+ + " req=" + requestCode + " res=" + resultCode + " data=" + data);
+ ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
+ list.add(new ResultInfo(id, requestCode, resultCode, data));
+ mAppThread.scheduleSendResult(token, list);
+ }
+
+ private void sendMessage(int what, Object obj) {
+ sendMessage(what, obj, 0, 0, false);
+ }
+
+ private void sendMessage(int what, Object obj, int arg1) {
+ sendMessage(what, obj, arg1, 0, false);
+ }
+
+ private void sendMessage(int what, Object obj, int arg1, int arg2) {
+ sendMessage(what, obj, arg1, arg2, false);
+ }
+
+ private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
+ if (DEBUG_MESSAGES) Slog.v(
+ TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
+ + ": " + arg1 + " / " + obj);
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.obj = obj;
+ msg.arg1 = arg1;
+ msg.arg2 = arg2;
+ if (async) {
+ msg.setAsynchronous(true);
+ }
+ mH.sendMessage(msg);
+ }
+
+ private void sendMessage(int what, Object obj, int arg1, int arg2, int seq) {
+ if (DEBUG_MESSAGES) Slog.v(
+ TAG, "SCHEDULE " + mH.codeToString(what) + " arg1=" + arg1 + " arg2=" + arg2 +
+ "seq= " + seq);
+ Message msg = Message.obtain();
+ msg.what = what;
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = obj;
+ args.argi1 = arg1;
+ args.argi2 = arg2;
+ args.argi3 = seq;
+ msg.obj = args;
+ mH.sendMessage(msg);
+ }
+
+ final void scheduleContextCleanup(ContextImpl context, String who,
+ String what) {
+ ContextCleanupInfo cci = new ContextCleanupInfo();
+ cci.context = context;
+ cci.who = who;
+ cci.what = what;
+ sendMessage(H.CLEAN_UP_CONTEXT, cci);
+ }
+
+ private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
+ // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
+
+ ActivityInfo aInfo = r.activityInfo;
+ if (r.packageInfo == null) {
+ r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
+ Context.CONTEXT_INCLUDE_CODE);
+ }
+
+ ComponentName component = r.intent.getComponent();
+ if (component == null) {
+ component = r.intent.resolveActivity(
+ mInitialApplication.getPackageManager());
+ r.intent.setComponent(component);
+ }
+
+ if (r.activityInfo.targetActivity != null) {
+ component = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.targetActivity);
+ }
+
+ ContextImpl appContext = createBaseContextForActivity(r);
+ Activity activity = null;
+ try {
+ java.lang.ClassLoader cl = appContext.getClassLoader();
+ activity = mInstrumentation.newActivity(
+ cl, component.getClassName(), r.intent);
+ StrictMode.incrementExpectedActivityCount(activity.getClass());
+ r.intent.setExtrasClassLoader(cl);
+ r.intent.prepareToEnterProcess();
+ if (r.state != null) {
+ r.state.setClassLoader(cl);
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(activity, e)) {
+ throw new RuntimeException(
+ "Unable to instantiate activity " + component
+ + ": " + e.toString(), e);
+ }
+ }
+
+ try {
+ Application app = r.packageInfo.makeApplication(false, mInstrumentation);
+
+ if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
+ if (localLOGV) Slog.v(
+ TAG, r + ": app=" + app
+ + ", appName=" + app.getPackageName()
+ + ", pkg=" + r.packageInfo.getPackageName()
+ + ", comp=" + r.intent.getComponent().toShortString()
+ + ", dir=" + r.packageInfo.getAppDir());
+
+ if (activity != null) {
+ CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
+ Configuration config = new Configuration(mCompatConfiguration);
+ if (r.overrideConfig != null) {
+ config.updateFrom(r.overrideConfig);
+ }
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ + r.activityInfo.name + " with config " + config);
+ Window window = null;
+ if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
+ window = r.mPendingRemoveWindow;
+ r.mPendingRemoveWindow = null;
+ r.mPendingRemoveWindowManager = null;
+ }
+ appContext.setOuterContext(activity);
+ activity.attach(appContext, this, getInstrumentation(), r.token,
+ r.ident, app, r.intent, r.activityInfo, title, r.parent,
+ r.embeddedID, r.lastNonConfigurationInstances, config,
+ r.referrer, r.voiceInteractor, window, r.configCallback);
+
+ if (customIntent != null) {
+ activity.mIntent = customIntent;
+ }
+ r.lastNonConfigurationInstances = null;
+ checkAndBlockForNetworkAccess();
+ activity.mStartedActivity = false;
+ int theme = r.activityInfo.getThemeResource();
+ if (theme != 0) {
+ activity.setTheme(theme);
+ }
+
+ activity.mCalled = false;
+ if (r.isPersistable()) {
+ mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
+ } else {
+ mInstrumentation.callActivityOnCreate(activity, r.state);
+ }
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onCreate()");
+ }
+ r.activity = activity;
+ r.stopped = true;
+ if (!r.activity.mFinished) {
+ activity.performStart();
+ r.stopped = false;
+ }
+ if (!r.activity.mFinished) {
+ if (r.isPersistable()) {
+ if (r.state != null || r.persistentState != null) {
+ mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
+ r.persistentState);
+ }
+ } else if (r.state != null) {
+ mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
+ }
+ }
+ if (!r.activity.mFinished) {
+ activity.mCalled = false;
+ if (r.isPersistable()) {
+ mInstrumentation.callActivityOnPostCreate(activity, r.state,
+ r.persistentState);
+ } else {
+ mInstrumentation.callActivityOnPostCreate(activity, r.state);
+ }
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onPostCreate()");
+ }
+ }
+ }
+ r.paused = true;
+
+ mActivities.put(r.token, r);
+
+ } catch (SuperNotCalledException e) {
+ throw e;
+
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(activity, e)) {
+ throw new RuntimeException(
+ "Unable to start activity " + component
+ + ": " + e.toString(), e);
+ }
+ }
+
+ return activity;
+ }
+
+ /**
+ * Checks if {@link #mNetworkBlockSeq} is {@link #INVALID_PROC_STATE_SEQ} and if so, returns
+ * immediately. Otherwise, makes a blocking call to ActivityManagerService to wait for the
+ * network rules to get updated.
+ */
+ private void checkAndBlockForNetworkAccess() {
+ synchronized (mNetworkPolicyLock) {
+ if (mNetworkBlockSeq != INVALID_PROC_STATE_SEQ) {
+ try {
+ ActivityManager.getService().waitForNetworkStateUpdate(mNetworkBlockSeq);
+ mNetworkBlockSeq = INVALID_PROC_STATE_SEQ;
+ } catch (RemoteException ignored) {}
+ }
+ }
+ }
+
+ private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
+ final int displayId;
+ try {
+ displayId = ActivityManager.getService().getActivityDisplayId(r.token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ ContextImpl appContext = ContextImpl.createActivityContext(
+ this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
+
+ final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
+ // For debugging purposes, if the activity's package name contains the value of
+ // the "debug.use-second-display" system property as a substring, then show
+ // its content on a secondary display if there is one.
+ String pkgName = SystemProperties.get("debug.second-display.pkg");
+ if (pkgName != null && !pkgName.isEmpty()
+ && r.packageInfo.mPackageName.contains(pkgName)) {
+ for (int id : dm.getDisplayIds()) {
+ if (id != Display.DEFAULT_DISPLAY) {
+ Display display =
+ dm.getCompatibleDisplay(id, appContext.getResources());
+ appContext = (ContextImpl) appContext.createDisplayContext(display);
+ break;
+ }
+ }
+ }
+ return appContext;
+ }
+
+ private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
+
+ if (r.profilerInfo != null) {
+ mProfiler.setProfiler(r.profilerInfo);
+ mProfiler.startProfiling();
+ }
+
+ // Make sure we are running with the most recent config.
+ handleConfigurationChanged(null, null);
+
+ if (localLOGV) Slog.v(
+ TAG, "Handling launch of " + r);
+
+ // Initialize before creating the activity
+ if (!ThreadedRenderer.sRendererDisabled) {
+ GraphicsEnvironment.earlyInitEGL();
+ }
+ WindowManagerGlobal.initialize();
+
+ Activity a = performLaunchActivity(r, customIntent);
+
+ if (a != null) {
+ r.createdConfig = new Configuration(mConfiguration);
+ reportSizeConfigurations(r);
+ Bundle oldState = r.state;
+ handleResumeActivity(r.token, false, r.isForward,
+ !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
+
+ if (!r.activity.mFinished && r.startsNotResumed) {
+ // The activity manager actually wants this one to start out paused, because it
+ // needs to be visible but isn't in the foreground. We accomplish this by going
+ // through the normal startup (because activities expect to go through onResume()
+ // the first time they run, before their window is displayed), and then pausing it.
+ // However, in this case we do -not- need to do the full pause cycle (of freezing
+ // and such) because the activity manager assumes it can just retain the current
+ // state it has.
+ performPauseActivityIfNeeded(r, reason);
+
+ // We need to keep around the original state, in case we need to be created again.
+ // But we only do this for pre-Honeycomb apps, which always save their state when
+ // pausing, so we can not have them save their state when restarting from a paused
+ // state. For HC and later, we want to (and can) let the state be saved as the
+ // normal part of stopping the activity.
+ if (r.isPreHoneycomb()) {
+ r.state = oldState;
+ }
+ }
+ } else {
+ // If there was an error, for any reason, tell the activity manager to stop us.
+ try {
+ ActivityManager.getService()
+ .finishActivity(r.token, Activity.RESULT_CANCELED, null,
+ Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private void reportSizeConfigurations(ActivityClientRecord r) {
+ Configuration[] configurations = r.activity.getResources().getSizeConfigurations();
+ if (configurations == null) {
+ return;
+ }
+ SparseIntArray horizontal = new SparseIntArray();
+ SparseIntArray vertical = new SparseIntArray();
+ SparseIntArray smallest = new SparseIntArray();
+ for (int i = configurations.length - 1; i >= 0; i--) {
+ Configuration config = configurations[i];
+ if (config.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+ vertical.put(config.screenHeightDp, 0);
+ }
+ if (config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+ horizontal.put(config.screenWidthDp, 0);
+ }
+ if (config.smallestScreenWidthDp != Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
+ smallest.put(config.smallestScreenWidthDp, 0);
+ }
+ }
+ try {
+ ActivityManager.getService().reportSizeConfigurations(r.token,
+ horizontal.copyKeys(), vertical.copyKeys(), smallest.copyKeys());
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
+ }
+
+ private void deliverNewIntents(ActivityClientRecord r, List<ReferrerIntent> intents) {
+ final int N = intents.size();
+ for (int i=0; i<N; i++) {
+ ReferrerIntent intent = intents.get(i);
+ intent.setExtrasClassLoader(r.activity.getClassLoader());
+ intent.prepareToEnterProcess();
+ r.activity.mFragments.noteStateNotSaved();
+ mInstrumentation.callActivityOnNewIntent(r.activity, intent);
+ }
+ }
+
+ void performNewIntents(IBinder token, List<ReferrerIntent> intents, boolean andPause) {
+ final ActivityClientRecord r = mActivities.get(token);
+ if (r == null) {
+ return;
+ }
+
+ final boolean resumed = !r.paused;
+ if (resumed) {
+ r.activity.mTemporaryPause = true;
+ mInstrumentation.callActivityOnPause(r.activity);
+ }
+ checkAndBlockForNetworkAccess();
+ deliverNewIntents(r, intents);
+ if (resumed) {
+ r.activity.performResume();
+ r.activity.mTemporaryPause = false;
+ }
+
+ if (r.paused && andPause) {
+ // In this case the activity was in the paused state when we delivered the intent,
+ // to guarantee onResume gets called after onNewIntent we temporarily resume the
+ // activity and pause again as the caller wanted.
+ performResumeActivity(token, false, "performNewIntents");
+ performPauseActivityIfNeeded(r, "performNewIntents");
+ }
+ }
+
+ private void handleNewIntent(NewIntentData data) {
+ performNewIntents(data.token, data.intents, data.andPause);
+ }
+
+ public void handleRequestAssistContextExtras(RequestAssistContextExtras cmd) {
+ // Filling for autofill has a few differences:
+ // - it does not need an AssistContent
+ // - it does not call onProvideAssistData()
+ // - it needs an IAutoFillCallback
+ boolean forAutofill = cmd.requestType == ActivityManager.ASSIST_CONTEXT_AUTOFILL;
+
+ // TODO: decide if lastSessionId logic applies to autofill sessions
+ if (mLastSessionId != cmd.sessionId) {
+ // Clear the existing structures
+ mLastSessionId = cmd.sessionId;
+ for (int i = mLastAssistStructures.size() - 1; i >= 0; i--) {
+ AssistStructure structure = mLastAssistStructures.get(i).get();
+ if (structure != null) {
+ structure.clearSendChannel();
+ }
+ mLastAssistStructures.remove(i);
+ }
+ }
+
+ Bundle data = new Bundle();
+ AssistStructure structure = null;
+ AssistContent content = forAutofill ? null : new AssistContent();
+ final long startTime = SystemClock.uptimeMillis();
+ ActivityClientRecord r = mActivities.get(cmd.activityToken);
+ Uri referrer = null;
+ if (r != null) {
+ if (!forAutofill) {
+ r.activity.getApplication().dispatchOnProvideAssistData(r.activity, data);
+ r.activity.onProvideAssistData(data);
+ referrer = r.activity.onProvideReferrer();
+ }
+ if (cmd.requestType == ActivityManager.ASSIST_CONTEXT_FULL || forAutofill) {
+ structure = new AssistStructure(r.activity, forAutofill, cmd.flags);
+ Intent activityIntent = r.activity.getIntent();
+ boolean notSecure = r.window == null ||
+ (r.window.getAttributes().flags
+ & WindowManager.LayoutParams.FLAG_SECURE) == 0;
+ if (activityIntent != null && notSecure) {
+ if (!forAutofill) {
+ Intent intent = new Intent(activityIntent);
+ intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
+ intent.removeUnsafeExtras();
+ content.setDefaultIntent(intent);
+ }
+ } else {
+ if (!forAutofill) {
+ content.setDefaultIntent(new Intent());
+ }
+ }
+ if (!forAutofill) {
+ r.activity.onProvideAssistContent(content);
+ }
+ }
+ }
+ if (structure == null) {
+ structure = new AssistStructure();
+ }
+
+ // TODO: decide if lastSessionId logic applies to autofill sessions
+
+ structure.setAcquisitionStartTime(startTime);
+ structure.setAcquisitionEndTime(SystemClock.uptimeMillis());
+
+ mLastAssistStructures.add(new WeakReference<>(structure));
+ IActivityManager mgr = ActivityManager.getService();
+ try {
+ mgr.reportAssistContextExtras(cmd.requestToken, data, structure, content, referrer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public void handleTranslucentConversionComplete(IBinder token, boolean drawComplete) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ r.activity.onTranslucentConversionComplete(drawComplete);
+ }
+ }
+
+ public void onNewActivityOptions(IBinder token, ActivityOptions options) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ r.activity.onNewActivityOptions(options);
+ }
+ }
+
+ public void handleInstallProvider(ProviderInfo info) {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ installContentProviders(mInitialApplication, Arrays.asList(info));
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private void handleEnterAnimationComplete(IBinder token) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ r.activity.dispatchEnterAnimationComplete();
+ }
+ }
+
+ private void handleStartBinderTracking() {
+ Binder.enableTracing();
+ }
+
+ private void handleStopBinderTrackingAndDump(ParcelFileDescriptor fd) {
+ try {
+ Binder.disableTracing();
+ Binder.getTransactionTracker().writeTracesToFile(fd);
+ } finally {
+ IoUtils.closeQuietly(fd);
+ Binder.getTransactionTracker().clearTraces();
+ }
+ }
+
+ private void handleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode,
+ Configuration overrideConfig) {
+ final ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ final Configuration newConfig = new Configuration(mConfiguration);
+ if (overrideConfig != null) {
+ newConfig.updateFrom(overrideConfig);
+ }
+ r.activity.dispatchMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+ }
+
+ private void handlePictureInPictureModeChanged(IBinder token, boolean isInPipMode,
+ Configuration overrideConfig) {
+ final ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ final Configuration newConfig = new Configuration(mConfiguration);
+ if (overrideConfig != null) {
+ newConfig.updateFrom(overrideConfig);
+ }
+ r.activity.dispatchPictureInPictureModeChanged(isInPipMode, newConfig);
+ }
+ }
+
+ private void handleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor interactor) {
+ final ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ r.voiceInteractor = interactor;
+ r.activity.setVoiceInteractor(interactor);
+ if (interactor == null) {
+ r.activity.onLocalVoiceInteractionStopped();
+ } else {
+ r.activity.onLocalVoiceInteractionStarted();
+ }
+ }
+ }
+
+ static final void handleAttachAgent(String agent) {
+ try {
+ VMDebug.attachAgent(agent);
+ } catch (IOException e) {
+ Slog.e(TAG, "Attaching agent failed: " + agent);
+ }
+ }
+
+ private static final ThreadLocal<Intent> sCurrentBroadcastIntent = new ThreadLocal<Intent>();
+
+ /**
+ * Return the Intent that's currently being handled by a
+ * BroadcastReceiver on this thread, or null if none.
+ * @hide
+ */
+ public static Intent getIntentBeingBroadcast() {
+ return sCurrentBroadcastIntent.get();
+ }
+
+ private void handleReceiver(ReceiverData data) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ String component = data.intent.getComponent().getClassName();
+
+ LoadedApk packageInfo = getPackageInfoNoCheck(
+ data.info.applicationInfo, data.compatInfo);
+
+ IActivityManager mgr = ActivityManager.getService();
+
+ Application app;
+ BroadcastReceiver receiver;
+ ContextImpl context;
+ try {
+ app = packageInfo.makeApplication(false, mInstrumentation);
+ context = (ContextImpl) app.getBaseContext();
+ if (data.info.splitName != null) {
+ context = (ContextImpl) context.createContextForSplit(data.info.splitName);
+ }
+ java.lang.ClassLoader cl = context.getClassLoader();
+ data.intent.setExtrasClassLoader(cl);
+ data.intent.prepareToEnterProcess();
+ data.setExtrasClassLoader(cl);
+ receiver = (BroadcastReceiver)cl.loadClass(component).newInstance();
+ } catch (Exception e) {
+ if (DEBUG_BROADCAST) Slog.i(TAG,
+ "Finishing failed broadcast to " + data.intent.getComponent());
+ data.sendFinished(mgr);
+ throw new RuntimeException(
+ "Unable to instantiate receiver " + component
+ + ": " + e.toString(), e);
+ }
+
+ try {
+ if (localLOGV) Slog.v(
+ TAG, "Performing receive of " + data.intent
+ + ": app=" + app
+ + ", appName=" + app.getPackageName()
+ + ", pkg=" + packageInfo.getPackageName()
+ + ", comp=" + data.intent.getComponent().toShortString()
+ + ", dir=" + packageInfo.getAppDir());
+
+ sCurrentBroadcastIntent.set(data.intent);
+ receiver.setPendingResult(data);
+ receiver.onReceive(context.getReceiverRestrictedContext(),
+ data.intent);
+ } catch (Exception e) {
+ if (DEBUG_BROADCAST) Slog.i(TAG,
+ "Finishing failed broadcast to " + data.intent.getComponent());
+ data.sendFinished(mgr);
+ if (!mInstrumentation.onException(receiver, e)) {
+ throw new RuntimeException(
+ "Unable to start receiver " + component
+ + ": " + e.toString(), e);
+ }
+ } finally {
+ sCurrentBroadcastIntent.set(null);
+ }
+
+ if (receiver.getPendingResult() != null) {
+ data.finish();
+ }
+ }
+
+ // Instantiate a BackupAgent and tell it that it's alive
+ private void handleCreateBackupAgent(CreateBackupAgentData data) {
+ if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data);
+
+ // Sanity check the requested target package's uid against ours
+ try {
+ PackageInfo requestedPackage = getPackageManager().getPackageInfo(
+ data.appInfo.packageName, 0, UserHandle.myUserId());
+ if (requestedPackage.applicationInfo.uid != Process.myUid()) {
+ Slog.w(TAG, "Asked to instantiate non-matching package "
+ + data.appInfo.packageName);
+ return;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ // no longer idle; we have backup work to do
+ unscheduleGcIdler();
+
+ // instantiate the BackupAgent class named in the manifest
+ LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
+ String packageName = packageInfo.mPackageName;
+ if (packageName == null) {
+ Slog.d(TAG, "Asked to create backup agent for nonexistent package");
+ return;
+ }
+
+ String classname = data.appInfo.backupAgentName;
+ // full backup operation but no app-supplied agent? use the default implementation
+ if (classname == null && (data.backupMode == ApplicationThreadConstants.BACKUP_MODE_FULL
+ || data.backupMode == ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL)) {
+ classname = "android.app.backup.FullBackupAgent";
+ }
+
+ try {
+ IBinder binder = null;
+ BackupAgent agent = mBackupAgents.get(packageName);
+ if (agent != null) {
+ // reusing the existing instance
+ if (DEBUG_BACKUP) {
+ Slog.v(TAG, "Reusing existing agent instance");
+ }
+ binder = agent.onBind();
+ } else {
+ try {
+ if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname);
+
+ java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ agent = (BackupAgent) cl.loadClass(classname).newInstance();
+
+ // set up the agent's context
+ ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
+ context.setOuterContext(agent);
+ agent.attach(context);
+
+ agent.onCreate();
+ binder = agent.onBind();
+ mBackupAgents.put(packageName, agent);
+ } catch (Exception e) {
+ // If this is during restore, fail silently; otherwise go
+ // ahead and let the user see the crash.
+ Slog.e(TAG, "Agent threw during creation: " + e);
+ if (data.backupMode != ApplicationThreadConstants.BACKUP_MODE_RESTORE
+ && data.backupMode !=
+ ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL) {
+ throw e;
+ }
+ // falling through with 'binder' still null
+ }
+ }
+
+ // tell the OS that we're live now
+ try {
+ ActivityManager.getService().backupAgentCreated(packageName, binder);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to create BackupAgent "
+ + classname + ": " + e.toString(), e);
+ }
+ }
+
+ // Tear down a BackupAgent
+ private void handleDestroyBackupAgent(CreateBackupAgentData data) {
+ if (DEBUG_BACKUP) Slog.v(TAG, "handleDestroyBackupAgent: " + data);
+
+ LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
+ String packageName = packageInfo.mPackageName;
+ BackupAgent agent = mBackupAgents.get(packageName);
+ if (agent != null) {
+ try {
+ agent.onDestroy();
+ } catch (Exception e) {
+ Slog.w(TAG, "Exception thrown in onDestroy by backup agent of " + data.appInfo);
+ e.printStackTrace();
+ }
+ mBackupAgents.remove(packageName);
+ } else {
+ Slog.w(TAG, "Attempt to destroy unknown backup agent " + data);
+ }
+ }
+
+ private void handleCreateService(CreateServiceData data) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ LoadedApk packageInfo = getPackageInfoNoCheck(
+ data.info.applicationInfo, data.compatInfo);
+ Service service = null;
+ try {
+ java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ service = (Service) cl.loadClass(data.info.name).newInstance();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(service, e)) {
+ throw new RuntimeException(
+ "Unable to instantiate service " + data.info.name
+ + ": " + e.toString(), e);
+ }
+ }
+
+ try {
+ if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);
+
+ ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
+ context.setOuterContext(service);
+
+ Application app = packageInfo.makeApplication(false, mInstrumentation);
+ service.attach(context, this, data.info.name, data.token, app,
+ ActivityManager.getService());
+ service.onCreate();
+ mServices.put(data.token, service);
+ try {
+ ActivityManager.getService().serviceDoneExecuting(
+ data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(service, e)) {
+ throw new RuntimeException(
+ "Unable to create service " + data.info.name
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ private void handleBindService(BindServiceData data) {
+ Service s = mServices.get(data.token);
+ if (DEBUG_SERVICE)
+ Slog.v(TAG, "handleBindService s=" + s + " rebind=" + data.rebind);
+ if (s != null) {
+ try {
+ data.intent.setExtrasClassLoader(s.getClassLoader());
+ data.intent.prepareToEnterProcess();
+ try {
+ if (!data.rebind) {
+ IBinder binder = s.onBind(data.intent);
+ ActivityManager.getService().publishService(
+ data.token, data.intent, binder);
+ } else {
+ s.onRebind(data.intent);
+ ActivityManager.getService().serviceDoneExecuting(
+ data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
+ }
+ ensureJitEnabled();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to bind to service " + s
+ + " with " + data.intent + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private void handleUnbindService(BindServiceData data) {
+ Service s = mServices.get(data.token);
+ if (s != null) {
+ try {
+ data.intent.setExtrasClassLoader(s.getClassLoader());
+ data.intent.prepareToEnterProcess();
+ boolean doRebind = s.onUnbind(data.intent);
+ try {
+ if (doRebind) {
+ ActivityManager.getService().unbindFinished(
+ data.token, data.intent, doRebind);
+ } else {
+ ActivityManager.getService().serviceDoneExecuting(
+ data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to unbind to service " + s
+ + " with " + data.intent + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private void handleDumpService(DumpComponentInfo info) {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ Service s = mServices.get(info.token);
+ if (s != null) {
+ PrintWriter pw = new FastPrintWriter(new FileOutputStream(
+ info.fd.getFileDescriptor()));
+ s.dump(info.fd.getFileDescriptor(), pw, info.args);
+ pw.flush();
+ }
+ } finally {
+ IoUtils.closeQuietly(info.fd);
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private void handleDumpActivity(DumpComponentInfo info) {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ ActivityClientRecord r = mActivities.get(info.token);
+ if (r != null && r.activity != null) {
+ PrintWriter pw = new FastPrintWriter(new FileOutputStream(
+ info.fd.getFileDescriptor()));
+ r.activity.dump(info.prefix, info.fd.getFileDescriptor(), pw, info.args);
+ pw.flush();
+ }
+ } finally {
+ IoUtils.closeQuietly(info.fd);
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private void handleDumpProvider(DumpComponentInfo info) {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ ProviderClientRecord r = mLocalProviders.get(info.token);
+ if (r != null && r.mLocalProvider != null) {
+ PrintWriter pw = new FastPrintWriter(new FileOutputStream(
+ info.fd.getFileDescriptor()));
+ r.mLocalProvider.dump(info.fd.getFileDescriptor(), pw, info.args);
+ pw.flush();
+ }
+ } finally {
+ IoUtils.closeQuietly(info.fd);
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private void handleServiceArgs(ServiceArgsData data) {
+ Service s = mServices.get(data.token);
+ if (s != null) {
+ try {
+ if (data.args != null) {
+ data.args.setExtrasClassLoader(s.getClassLoader());
+ data.args.prepareToEnterProcess();
+ }
+ int res;
+ if (!data.taskRemoved) {
+ res = s.onStartCommand(data.args, data.flags, data.startId);
+ } else {
+ s.onTaskRemoved(data.args);
+ res = Service.START_TASK_REMOVED_COMPLETE;
+ }
+
+ QueuedWork.waitToFinish();
+
+ try {
+ ActivityManager.getService().serviceDoneExecuting(
+ data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ ensureJitEnabled();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to start service " + s
+ + " with " + data.args + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private void handleStopService(IBinder token) {
+ Service s = mServices.remove(token);
+ if (s != null) {
+ try {
+ if (localLOGV) Slog.v(TAG, "Destroying service " + s);
+ s.onDestroy();
+ s.detachAndCleanUp();
+ Context context = s.getBaseContext();
+ if (context instanceof ContextImpl) {
+ final String who = s.getClassName();
+ ((ContextImpl) context).scheduleFinalCleanup(who, "Service");
+ }
+
+ QueuedWork.waitToFinish();
+
+ try {
+ ActivityManager.getService().serviceDoneExecuting(
+ token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to stop service " + s
+ + ": " + e.toString(), e);
+ }
+ Slog.i(TAG, "handleStopService: exception for " + token, e);
+ }
+ } else {
+ Slog.i(TAG, "handleStopService: token=" + token + " not found.");
+ }
+ //Slog.i(TAG, "Running services: " + mServices);
+ }
+
+ public final ActivityClientRecord performResumeActivity(IBinder token,
+ boolean clearHide, String reason) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (localLOGV) Slog.v(TAG, "Performing resume of " + r
+ + " finished=" + r.activity.mFinished);
+ if (r != null && !r.activity.mFinished) {
+ if (clearHide) {
+ r.hideForNow = false;
+ r.activity.mStartedActivity = false;
+ }
+ try {
+ r.activity.onStateNotSaved();
+ r.activity.mFragments.noteStateNotSaved();
+ checkAndBlockForNetworkAccess();
+ if (r.pendingIntents != null) {
+ deliverNewIntents(r, r.pendingIntents);
+ r.pendingIntents = null;
+ }
+ if (r.pendingResults != null) {
+ deliverResults(r, r.pendingResults);
+ r.pendingResults = null;
+ }
+ r.activity.performResume();
+
+ synchronized (mResourcesManager) {
+ // If there is a pending local relaunch that was requested when the activity was
+ // paused, it will put the activity into paused state when it finally happens.
+ // Since the activity resumed before being relaunched, we don't want that to
+ // happen, so we need to clear the request to relaunch paused.
+ for (int i = mRelaunchingActivities.size() - 1; i >= 0; i--) {
+ final ActivityClientRecord relaunching = mRelaunchingActivities.get(i);
+ if (relaunching.token == r.token
+ && relaunching.onlyLocalRequest && relaunching.startsNotResumed) {
+ relaunching.startsNotResumed = false;
+ }
+ }
+ }
+
+ EventLog.writeEvent(LOG_AM_ON_RESUME_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), reason);
+
+ r.paused = false;
+ r.stopped = false;
+ r.state = null;
+ r.persistentState = null;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to resume activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ return r;
+ }
+
+ static final void cleanUpPendingRemoveWindows(ActivityClientRecord r, boolean force) {
+ if (r.mPreserveWindow && !force) {
+ return;
+ }
+ if (r.mPendingRemoveWindow != null) {
+ r.mPendingRemoveWindowManager.removeViewImmediate(
+ r.mPendingRemoveWindow.getDecorView());
+ IBinder wtoken = r.mPendingRemoveWindow.getDecorView().getWindowToken();
+ if (wtoken != null) {
+ WindowManagerGlobal.getInstance().closeAll(wtoken,
+ r.activity.getClass().getName(), "Activity");
+ }
+ }
+ r.mPendingRemoveWindow = null;
+ r.mPendingRemoveWindowManager = null;
+ }
+
+ final void handleResumeActivity(IBinder token,
+ boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
+ return;
+ }
+
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
+
+ // TODO Push resumeArgs into the activity for consideration
+ r = performResumeActivity(token, clearHide, reason);
+
+ if (r != null) {
+ final Activity a = r.activity;
+
+ if (localLOGV) Slog.v(
+ TAG, "Resume " + r + " started activity: " +
+ a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ + ", finished: " + a.mFinished);
+
+ final int forwardBit = isForward ?
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
+
+ // If the window hasn't yet been added to the window manager,
+ // and this guy didn't finish itself or start another activity,
+ // then go ahead and add the window.
+ boolean willBeVisible = !a.mStartedActivity;
+ if (!willBeVisible) {
+ try {
+ willBeVisible = ActivityManager.getService().willActivityBeVisible(
+ a.getActivityToken());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ if (r.window == null && !a.mFinished && willBeVisible) {
+ r.window = r.activity.getWindow();
+ View decor = r.window.getDecorView();
+ decor.setVisibility(View.INVISIBLE);
+ ViewManager wm = a.getWindowManager();
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ a.mDecor = decor;
+ l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ l.softInputMode |= forwardBit;
+ if (r.mPreserveWindow) {
+ a.mWindowAdded = true;
+ r.mPreserveWindow = false;
+ // Normally the ViewRoot sets up callbacks with the Activity
+ // in addView->ViewRootImpl#setView. If we are instead reusing
+ // the decor view we have to notify the view root that the
+ // callbacks may have changed.
+ ViewRootImpl impl = decor.getViewRootImpl();
+ if (impl != null) {
+ impl.notifyChildRebuilt();
+ }
+ }
+ if (a.mVisibleFromClient) {
+ if (!a.mWindowAdded) {
+ a.mWindowAdded = true;
+ wm.addView(decor, l);
+ } else {
+ // The activity will get a callback for this {@link LayoutParams} change
+ // earlier. However, at that time the decor will not be set (this is set
+ // in this method), so no action will be taken. This call ensures the
+ // callback occurs with the decor set.
+ a.onWindowAttributesChanged(l);
+ }
+ }
+
+ // If the window has already been added, but during resume
+ // we started another activity, then don't yet make the
+ // window visible.
+ } else if (!willBeVisible) {
+ if (localLOGV) Slog.v(
+ TAG, "Launch " + r + " mStartedActivity set");
+ r.hideForNow = true;
+ }
+
+ // Get rid of anything left hanging around.
+ cleanUpPendingRemoveWindows(r, false /* force */);
+
+ // The window is now visible if it has been added, we are not
+ // simply finishing, and we are not starting another activity.
+ if (!r.activity.mFinished && willBeVisible
+ && r.activity.mDecor != null && !r.hideForNow) {
+ if (r.newConfig != null) {
+ performConfigurationChangedForActivity(r, r.newConfig);
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
+ + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig);
+ r.newConfig = null;
+ }
+ if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
+ + isForward);
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ if ((l.softInputMode
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
+ != forwardBit) {
+ l.softInputMode = (l.softInputMode
+ & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
+ | forwardBit;
+ if (r.activity.mVisibleFromClient) {
+ ViewManager wm = a.getWindowManager();
+ View decor = r.window.getDecorView();
+ wm.updateViewLayout(decor, l);
+ }
+ }
+
+ r.activity.mVisibleFromServer = true;
+ mNumVisibleActivities++;
+ if (r.activity.mVisibleFromClient) {
+ r.activity.makeVisible();
+ }
+ }
+
+ if (!r.onlyLocalRequest) {
+ r.nextIdle = mNewActivities;
+ mNewActivities = r;
+ if (localLOGV) Slog.v(
+ TAG, "Scheduling idle handler for " + r);
+ Looper.myQueue().addIdleHandler(new Idler());
+ }
+ r.onlyLocalRequest = false;
+
+ // Tell the activity manager we have resumed.
+ if (reallyResume) {
+ try {
+ ActivityManager.getService().activityResumed(token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ } else {
+ // If an exception was thrown when trying to resume, then
+ // just end this activity.
+ try {
+ ActivityManager.getService()
+ .finishActivity(token, Activity.RESULT_CANCELED, null,
+ Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private int mThumbnailWidth = -1;
+ private int mThumbnailHeight = -1;
+ private Bitmap mAvailThumbnailBitmap = null;
+ private Canvas mThumbnailCanvas = null;
+
+ private Bitmap createThumbnailBitmap(ActivityClientRecord r) {
+ Bitmap thumbnail = mAvailThumbnailBitmap;
+ try {
+ if (thumbnail == null) {
+ int w = mThumbnailWidth;
+ int h;
+ if (w < 0) {
+ Resources res = r.activity.getResources();
+ int wId = com.android.internal.R.dimen.thumbnail_width;
+ int hId = com.android.internal.R.dimen.thumbnail_height;
+ mThumbnailWidth = w = res.getDimensionPixelSize(wId);
+ mThumbnailHeight = h = res.getDimensionPixelSize(hId);
+ } else {
+ h = mThumbnailHeight;
+ }
+
+ // On platforms where we don't want thumbnails, set dims to (0,0)
+ if ((w > 0) && (h > 0)) {
+ thumbnail = Bitmap.createBitmap(r.activity.getResources().getDisplayMetrics(),
+ w, h, THUMBNAIL_FORMAT);
+ thumbnail.eraseColor(0);
+ }
+ }
+
+ if (thumbnail != null) {
+ Canvas cv = mThumbnailCanvas;
+ if (cv == null) {
+ mThumbnailCanvas = cv = new Canvas();
+ }
+
+ cv.setBitmap(thumbnail);
+ if (!r.activity.onCreateThumbnail(thumbnail, cv)) {
+ mAvailThumbnailBitmap = thumbnail;
+ thumbnail = null;
+ }
+ cv.setBitmap(null);
+ }
+
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to create thumbnail of "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ thumbnail = null;
+ }
+
+ return thumbnail;
+ }
+
+ private void handlePauseActivity(IBinder token, boolean finished,
+ boolean userLeaving, int configChanges, boolean dontReport, int seq) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (DEBUG_ORDER) Slog.d(TAG, "handlePauseActivity " + r + ", seq: " + seq);
+ if (!checkAndUpdateLifecycleSeq(seq, r, "pauseActivity")) {
+ return;
+ }
+ if (r != null) {
+ //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r);
+ if (userLeaving) {
+ performUserLeavingActivity(r);
+ }
+
+ r.activity.mConfigChangeFlags |= configChanges;
+ performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");
+
+ // Make sure any pending writes are now committed.
+ if (r.isPreHoneycomb()) {
+ QueuedWork.waitToFinish();
+ }
+
+ // Tell the activity manager we have paused.
+ if (!dontReport) {
+ try {
+ ActivityManager.getService().activityPaused(token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ mSomeActivitiesChanged = true;
+ }
+ }
+
+ final void performUserLeavingActivity(ActivityClientRecord r) {
+ mInstrumentation.callActivityOnUserLeaving(r.activity);
+ }
+
+ final Bundle performPauseActivity(IBinder token, boolean finished,
+ boolean saveState, String reason) {
+ ActivityClientRecord r = mActivities.get(token);
+ return r != null ? performPauseActivity(r, finished, saveState, reason) : null;
+ }
+
+ final Bundle performPauseActivity(ActivityClientRecord r, boolean finished,
+ boolean saveState, String reason) {
+ if (r.paused) {
+ if (r.activity.mFinished) {
+ // If we are finishing, we won't call onResume() in certain cases.
+ // So here we likewise don't want to call onPause() if the activity
+ // isn't resumed.
+ return null;
+ }
+ RuntimeException e = new RuntimeException(
+ "Performing pause of activity that is not resumed: "
+ + r.intent.getComponent().toShortString());
+ Slog.e(TAG, e.getMessage(), e);
+ }
+ if (finished) {
+ r.activity.mFinished = true;
+ }
+
+ // Next have the activity save its current state and managed dialogs...
+ if (!r.activity.mFinished && saveState) {
+ callCallActivityOnSaveInstanceState(r);
+ }
+
+ performPauseActivityIfNeeded(r, reason);
+
+ // Notify any outstanding on paused listeners
+ ArrayList<OnActivityPausedListener> listeners;
+ synchronized (mOnPauseListeners) {
+ listeners = mOnPauseListeners.remove(r.activity);
+ }
+ int size = (listeners != null ? listeners.size() : 0);
+ for (int i = 0; i < size; i++) {
+ listeners.get(i).onPaused(r.activity);
+ }
+
+ return !r.activity.mFinished && saveState ? r.state : null;
+ }
+
+ private void performPauseActivityIfNeeded(ActivityClientRecord r, String reason) {
+ if (r.paused) {
+ // You are already paused silly...
+ return;
+ }
+
+ try {
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnPause(r.activity);
+ EventLog.writeEvent(LOG_AM_ON_PAUSE_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), reason);
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException("Activity " + safeToComponentShortString(r.intent)
+ + " did not call through to super.onPause()");
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException("Unable to pause activity "
+ + safeToComponentShortString(r.intent) + ": " + e.toString(), e);
+ }
+ }
+ r.paused = true;
+ }
+
+ final void performStopActivity(IBinder token, boolean saveState, String reason) {
+ ActivityClientRecord r = mActivities.get(token);
+ performStopActivityInner(r, null, false, saveState, reason);
+ }
+
+ private static class StopInfo implements Runnable {
+ ActivityClientRecord activity;
+ Bundle state;
+ PersistableBundle persistentState;
+ CharSequence description;
+
+ @Override public void run() {
+ // Tell activity manager we have been stopped.
+ try {
+ if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
+ ActivityManager.getService().activityStopped(
+ activity.token, state, persistentState, description);
+ } catch (RemoteException ex) {
+ // Dump statistics about bundle to help developers debug
+ final LogWriter writer = new LogWriter(Log.WARN, TAG);
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ pw.println("Bundle stats:");
+ Bundle.dumpStats(pw, state);
+ pw.println("PersistableBundle stats:");
+ Bundle.dumpStats(pw, persistentState);
+
+ if (ex instanceof TransactionTooLargeException
+ && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
+ Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
+ return;
+ }
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private static final class ProviderRefCount {
+ public final ContentProviderHolder holder;
+ public final ProviderClientRecord client;
+ public int stableCount;
+ public int unstableCount;
+
+ // When this is set, the stable and unstable ref counts are 0 and
+ // we have a pending operation scheduled to remove the ref count
+ // from the activity manager. On the activity manager we are still
+ // holding an unstable ref, though it is not reflected in the counts
+ // here.
+ public boolean removePending;
+
+ ProviderRefCount(ContentProviderHolder inHolder,
+ ProviderClientRecord inClient, int sCount, int uCount) {
+ holder = inHolder;
+ client = inClient;
+ stableCount = sCount;
+ unstableCount = uCount;
+ }
+ }
+
+ /**
+ * Core implementation of stopping an activity. Note this is a little
+ * tricky because the server's meaning of stop is slightly different
+ * than our client -- for the server, stop means to save state and give
+ * it the result when it is done, but the window may still be visible.
+ * For the client, we want to call onStop()/onStart() to indicate when
+ * the activity's UI visibility changes.
+ */
+ private void performStopActivityInner(ActivityClientRecord r,
+ StopInfo info, boolean keepShown, boolean saveState, String reason) {
+ if (localLOGV) Slog.v(TAG, "Performing stop of " + r);
+ if (r != null) {
+ if (!keepShown && r.stopped) {
+ if (r.activity.mFinished) {
+ // If we are finishing, we won't call onResume() in certain
+ // cases. So here we likewise don't want to call onStop()
+ // if the activity isn't resumed.
+ return;
+ }
+ RuntimeException e = new RuntimeException(
+ "Performing stop of activity that is already stopped: "
+ + r.intent.getComponent().toShortString());
+ Slog.e(TAG, e.getMessage(), e);
+ Slog.e(TAG, r.getStateString());
+ }
+
+ // One must first be paused before stopped...
+ performPauseActivityIfNeeded(r, reason);
+
+ if (info != null) {
+ try {
+ // First create a thumbnail for the activity...
+ // For now, don't create the thumbnail here; we are
+ // doing that by doing a screen snapshot.
+ info.description = r.activity.onCreateDescription();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to save state of activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ // Next have the activity save its current state and managed dialogs...
+ if (!r.activity.mFinished && saveState) {
+ if (r.state == null) {
+ callCallActivityOnSaveInstanceState(r);
+ }
+ }
+
+ if (!keepShown) {
+ try {
+ // Now we are idle.
+ r.activity.performStop(false /*preserveWindow*/);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.stopped = true;
+ EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), reason);
+ }
+ }
+ }
+
+ private void updateVisibility(ActivityClientRecord r, boolean show) {
+ View v = r.activity.mDecor;
+ if (v != null) {
+ if (show) {
+ if (!r.activity.mVisibleFromServer) {
+ r.activity.mVisibleFromServer = true;
+ mNumVisibleActivities++;
+ if (r.activity.mVisibleFromClient) {
+ r.activity.makeVisible();
+ }
+ }
+ if (r.newConfig != null) {
+ performConfigurationChangedForActivity(r, r.newConfig);
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Updating activity vis "
+ + r.activityInfo.name + " with new config "
+ + r.activity.mCurrentConfig);
+ r.newConfig = null;
+ }
+ } else {
+ if (r.activity.mVisibleFromServer) {
+ r.activity.mVisibleFromServer = false;
+ mNumVisibleActivities--;
+ v.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+ }
+
+ private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
+ return;
+ }
+ r.activity.mConfigChangeFlags |= configChanges;
+
+ StopInfo info = new StopInfo();
+ performStopActivityInner(r, info, show, true, "handleStopActivity");
+
+ if (localLOGV) Slog.v(
+ TAG, "Finishing stop of " + r + ": show=" + show
+ + " win=" + r.window);
+
+ updateVisibility(r, show);
+
+ // Make sure any pending writes are now committed.
+ if (!r.isPreHoneycomb()) {
+ QueuedWork.waitToFinish();
+ }
+
+ // Schedule the call to tell the activity manager we have
+ // stopped. We don't do this immediately, because we want to
+ // have a chance for any other pending work (in particular memory
+ // trim requests) to complete before you tell the activity
+ // manager to proceed and allow us to go fully into the background.
+ info.activity = r;
+ info.state = r.state;
+ info.persistentState = r.persistentState;
+ mH.post(info);
+ mSomeActivitiesChanged = true;
+ }
+
+ private static boolean checkAndUpdateLifecycleSeq(int seq, ActivityClientRecord r,
+ String action) {
+ if (r == null) {
+ return true;
+ }
+ if (seq < r.lastProcessedSeq) {
+ if (DEBUG_ORDER) Slog.d(TAG, action + " for " + r + " ignored, because seq=" + seq
+ + " < mCurrentLifecycleSeq=" + r.lastProcessedSeq);
+ return false;
+ }
+ r.lastProcessedSeq = seq;
+ return true;
+ }
+
+ final void performRestartActivity(IBinder token) {
+ ActivityClientRecord r = mActivities.get(token);
+ if (r.stopped) {
+ r.activity.performRestart();
+ r.stopped = false;
+ }
+ }
+
+ private void handleWindowVisibility(IBinder token, boolean show) {
+ ActivityClientRecord r = mActivities.get(token);
+
+ if (r == null) {
+ Log.w(TAG, "handleWindowVisibility: no activity for token " + token);
+ return;
+ }
+
+ if (!show && !r.stopped) {
+ performStopActivityInner(r, null, show, false, "handleWindowVisibility");
+ } else if (show && r.stopped) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ r.activity.performRestart();
+ r.stopped = false;
+ }
+ if (r.activity.mDecor != null) {
+ if (false) Slog.v(
+ TAG, "Handle window " + r + " visibility: " + show);
+ updateVisibility(r, show);
+ }
+ mSomeActivitiesChanged = true;
+ }
+
+ // TODO: This method should be changed to use {@link #performStopActivityInner} to perform to
+ // stop operation on the activity to reduce code duplication and the chance of fixing a bug in
+ // one place and missing the other.
+ private void handleSleeping(IBinder token, boolean sleeping) {
+ ActivityClientRecord r = mActivities.get(token);
+
+ if (r == null) {
+ Log.w(TAG, "handleSleeping: no activity for token " + token);
+ return;
+ }
+
+ if (sleeping) {
+ if (!r.stopped && !r.isPreHoneycomb()) {
+ if (!r.activity.mFinished && r.state == null) {
+ callCallActivityOnSaveInstanceState(r);
+ }
+
+ try {
+ // Now we are idle.
+ r.activity.performStop(false /*preserveWindow*/);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.stopped = true;
+ EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), "sleeping");
+ }
+
+ // Make sure any pending writes are now committed.
+ if (!r.isPreHoneycomb()) {
+ QueuedWork.waitToFinish();
+ }
+
+ // Tell activity manager we slept.
+ try {
+ ActivityManager.getService().activitySlept(r.token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ } else {
+ if (r.stopped && r.activity.mVisibleFromServer) {
+ r.activity.performRestart();
+ r.stopped = false;
+ }
+ }
+ }
+
+ private void handleSetCoreSettings(Bundle coreSettings) {
+ synchronized (mResourcesManager) {
+ mCoreSettings = coreSettings;
+ }
+ onCoreSettingsChange();
+ }
+
+ private void onCoreSettingsChange() {
+ boolean debugViewAttributes =
+ mCoreSettings.getInt(Settings.Global.DEBUG_VIEW_ATTRIBUTES, 0) != 0;
+ if (debugViewAttributes != View.mDebugViewAttributes) {
+ View.mDebugViewAttributes = debugViewAttributes;
+
+ // request all activities to relaunch for the changes to take place
+ requestRelaunchAllActivities();
+ }
+ }
+
+ private void requestRelaunchAllActivities() {
+ for (Map.Entry<IBinder, ActivityClientRecord> entry : mActivities.entrySet()) {
+ final Activity activity = entry.getValue().activity;
+ if (!activity.mFinished) {
+ try {
+ ActivityManager.getService().requestActivityRelaunch(entry.getKey());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ private void handleUpdatePackageCompatibilityInfo(UpdateCompatibilityData data) {
+ LoadedApk apk = peekPackageInfo(data.pkg, false);
+ if (apk != null) {
+ apk.setCompatibilityInfo(data.info);
+ }
+ apk = peekPackageInfo(data.pkg, true);
+ if (apk != null) {
+ apk.setCompatibilityInfo(data.info);
+ }
+ handleConfigurationChanged(mConfiguration, data.info);
+ WindowManagerGlobal.getInstance().reportNewConfiguration(mConfiguration);
+ }
+
+ private void deliverResults(ActivityClientRecord r, List<ResultInfo> results) {
+ final int N = results.size();
+ for (int i=0; i<N; i++) {
+ ResultInfo ri = results.get(i);
+ try {
+ if (ri.mData != null) {
+ ri.mData.setExtrasClassLoader(r.activity.getClassLoader());
+ ri.mData.prepareToEnterProcess();
+ }
+ if (DEBUG_RESULTS) Slog.v(TAG,
+ "Delivering result to activity " + r + " : " + ri);
+ r.activity.dispatchActivityResult(ri.mResultWho,
+ ri.mRequestCode, ri.mResultCode, ri.mData);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Failure delivering result " + ri + " to activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private void handleSendResult(ResultData res) {
+ ActivityClientRecord r = mActivities.get(res.token);
+ if (DEBUG_RESULTS) Slog.v(TAG, "Handling send result to " + r);
+ if (r != null) {
+ final boolean resumed = !r.paused;
+ if (!r.activity.mFinished && r.activity.mDecor != null
+ && r.hideForNow && resumed) {
+ // We had hidden the activity because it started another
+ // one... we have gotten a result back and we are not
+ // paused, so make sure our window is visible.
+ updateVisibility(r, true);
+ }
+ if (resumed) {
+ try {
+ // Now we are idle.
+ r.activity.mCalled = false;
+ r.activity.mTemporaryPause = true;
+ mInstrumentation.callActivityOnPause(r.activity);
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString()
+ + " did not call through to super.onPause()");
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to pause activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ checkAndBlockForNetworkAccess();
+ deliverResults(r, res.results);
+ if (resumed) {
+ r.activity.performResume();
+ r.activity.mTemporaryPause = false;
+ }
+ }
+ }
+
+ public final ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing) {
+ return performDestroyActivity(token, finishing, 0, false);
+ }
+
+ private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
+ int configChanges, boolean getNonConfigInstance) {
+ ActivityClientRecord r = mActivities.get(token);
+ Class<? extends Activity> activityClass = null;
+ if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
+ if (r != null) {
+ activityClass = r.activity.getClass();
+ r.activity.mConfigChangeFlags |= configChanges;
+ if (finishing) {
+ r.activity.mFinished = true;
+ }
+
+ performPauseActivityIfNeeded(r, "destroy");
+
+ if (!r.stopped) {
+ try {
+ r.activity.performStop(r.mPreserveWindow);
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + safeToComponentShortString(r.intent)
+ + ": " + e.toString(), e);
+ }
+ }
+ r.stopped = true;
+ EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
+ r.activity.getComponentName().getClassName(), "destroy");
+ }
+ if (getNonConfigInstance) {
+ try {
+ r.lastNonConfigurationInstances
+ = r.activity.retainNonConfigurationInstances();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to retain activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ try {
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnDestroy(r.activity);
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + safeToComponentShortString(r.intent) +
+ " did not call through to super.onDestroy()");
+ }
+ if (r.window != null) {
+ r.window.closeAllPanels();
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to destroy activity " + safeToComponentShortString(r.intent)
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ mActivities.remove(token);
+ StrictMode.decrementExpectedActivityCount(activityClass);
+ return r;
+ }
+
+ private static String safeToComponentShortString(Intent intent) {
+ ComponentName component = intent.getComponent();
+ return component == null ? "[Unknown]" : component.toShortString();
+ }
+
+ private void handleDestroyActivity(IBinder token, boolean finishing,
+ int configChanges, boolean getNonConfigInstance) {
+ ActivityClientRecord r = performDestroyActivity(token, finishing,
+ configChanges, getNonConfigInstance);
+ if (r != null) {
+ cleanUpPendingRemoveWindows(r, finishing);
+ WindowManager wm = r.activity.getWindowManager();
+ View v = r.activity.mDecor;
+ if (v != null) {
+ if (r.activity.mVisibleFromServer) {
+ mNumVisibleActivities--;
+ }
+ IBinder wtoken = v.getWindowToken();
+ if (r.activity.mWindowAdded) {
+ if (r.mPreserveWindow) {
+ // Hold off on removing this until the new activity's
+ // window is being added.
+ r.mPendingRemoveWindow = r.window;
+ r.mPendingRemoveWindowManager = wm;
+ // We can only keep the part of the view hierarchy that we control,
+ // everything else must be removed, because it might not be able to
+ // behave properly when activity is relaunching.
+ r.window.clearContentView();
+ } else {
+ wm.removeViewImmediate(v);
+ }
+ }
+ if (wtoken != null && r.mPendingRemoveWindow == null) {
+ WindowManagerGlobal.getInstance().closeAll(wtoken,
+ r.activity.getClass().getName(), "Activity");
+ } else if (r.mPendingRemoveWindow != null) {
+ // We're preserving only one window, others should be closed so app views
+ // will be detached before the final tear down. It should be done now because
+ // some components (e.g. WebView) rely on detach callbacks to perform receiver
+ // unregister and other cleanup.
+ WindowManagerGlobal.getInstance().closeAllExceptView(token, v,
+ r.activity.getClass().getName(), "Activity");
+ }
+ r.activity.mDecor = null;
+ }
+ if (r.mPendingRemoveWindow == null) {
+ // If we are delaying the removal of the activity window, then
+ // we can't clean up all windows here. Note that we can't do
+ // so later either, which means any windows that aren't closed
+ // by the app will leak. Well we try to warning them a lot
+ // about leaking windows, because that is a bug, so if they are
+ // using this recreate facility then they get to live with leaks.
+ WindowManagerGlobal.getInstance().closeAll(token,
+ r.activity.getClass().getName(), "Activity");
+ }
+
+ // Mocked out contexts won't be participating in the normal
+ // process lifecycle, but if we're running with a proper
+ // ApplicationContext we need to have it tear down things
+ // cleanly.
+ Context c = r.activity.getBaseContext();
+ if (c instanceof ContextImpl) {
+ ((ContextImpl) c).scheduleFinalCleanup(
+ r.activity.getClass().getName(), "Activity");
+ }
+ }
+ if (finishing) {
+ try {
+ ActivityManager.getService().activityDestroyed(token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ mSomeActivitiesChanged = true;
+ }
+
+ /**
+ * @param preserveWindow Whether the activity should try to reuse the window it created,
+ * including the decor view after the relaunch.
+ */
+ public final void requestRelaunchActivity(IBinder token,
+ List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
+ int configChanges, boolean notResumed, Configuration config,
+ Configuration overrideConfig, boolean fromServer, boolean preserveWindow) {
+ ActivityClientRecord target = null;
+
+ synchronized (mResourcesManager) {
+ for (int i=0; i<mRelaunchingActivities.size(); i++) {
+ ActivityClientRecord r = mRelaunchingActivities.get(i);
+ if (DEBUG_ORDER) Slog.d(TAG, "requestRelaunchActivity: " + this + ", trying: " + r);
+ if (r.token == token) {
+ target = r;
+ if (pendingResults != null) {
+ if (r.pendingResults != null) {
+ r.pendingResults.addAll(pendingResults);
+ } else {
+ r.pendingResults = pendingResults;
+ }
+ }
+ if (pendingNewIntents != null) {
+ if (r.pendingIntents != null) {
+ r.pendingIntents.addAll(pendingNewIntents);
+ } else {
+ r.pendingIntents = pendingNewIntents;
+ }
+ }
+
+ // For each relaunch request, activity manager expects an answer
+ if (!r.onlyLocalRequest && fromServer) {
+ try {
+ ActivityManager.getService().activityRelaunched(token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ break;
+ }
+ }
+
+ if (target == null) {
+ if (DEBUG_ORDER) Slog.d(TAG, "requestRelaunchActivity: target is null, fromServer:"
+ + fromServer);
+ target = new ActivityClientRecord();
+ target.token = token;
+ target.pendingResults = pendingResults;
+ target.pendingIntents = pendingNewIntents;
+ target.mPreserveWindow = preserveWindow;
+ if (!fromServer) {
+ final ActivityClientRecord existing = mActivities.get(token);
+ if (DEBUG_ORDER) Slog.d(TAG, "requestRelaunchActivity: " + existing);
+ if (existing != null) {
+ if (DEBUG_ORDER) Slog.d(TAG, "requestRelaunchActivity: paused= "
+ + existing.paused);;
+ target.startsNotResumed = existing.paused;
+ target.overrideConfig = existing.overrideConfig;
+ }
+ target.onlyLocalRequest = true;
+ }
+ mRelaunchingActivities.add(target);
+ sendMessage(H.RELAUNCH_ACTIVITY, target);
+ }
+
+ if (fromServer) {
+ target.startsNotResumed = notResumed;
+ target.onlyLocalRequest = false;
+ }
+ if (config != null) {
+ target.createdConfig = config;
+ }
+ if (overrideConfig != null) {
+ target.overrideConfig = overrideConfig;
+ }
+ target.pendingConfigChanges |= configChanges;
+ target.relaunchSeq = getLifecycleSeq();
+ }
+ if (DEBUG_ORDER) Slog.d(TAG, "relaunchActivity " + ActivityThread.this + ", target "
+ + target + " operation received seq: " + target.relaunchSeq);
+ }
+
+ private void handleRelaunchActivity(ActivityClientRecord tmp) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
+
+ Configuration changedConfig = null;
+ int configChanges = 0;
+
+ // First: make sure we have the most recent configuration and most
+ // recent version of the activity, or skip it if some previous call
+ // had taken a more recent version.
+ synchronized (mResourcesManager) {
+ int N = mRelaunchingActivities.size();
+ IBinder token = tmp.token;
+ tmp = null;
+ for (int i=0; i<N; i++) {
+ ActivityClientRecord r = mRelaunchingActivities.get(i);
+ if (r.token == token) {
+ tmp = r;
+ configChanges |= tmp.pendingConfigChanges;
+ mRelaunchingActivities.remove(i);
+ i--;
+ N--;
+ }
+ }
+
+ if (tmp == null) {
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Abort, activity not relaunching!");
+ return;
+ }
+
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Relaunching activity "
+ + tmp.token + " with configChanges=0x"
+ + Integer.toHexString(configChanges));
+
+ if (mPendingConfiguration != null) {
+ changedConfig = mPendingConfiguration;
+ mPendingConfiguration = null;
+ }
+ }
+
+ if (tmp.lastProcessedSeq > tmp.relaunchSeq) {
+ Slog.wtf(TAG, "For some reason target: " + tmp + " has lower sequence: "
+ + tmp.relaunchSeq + " than current sequence: " + tmp.lastProcessedSeq);
+ } else {
+ tmp.lastProcessedSeq = tmp.relaunchSeq;
+ }
+ if (tmp.createdConfig != null) {
+ // If the activity manager is passing us its current config,
+ // assume that is really what we want regardless of what we
+ // may have pending.
+ if (mConfiguration == null
+ || (tmp.createdConfig.isOtherSeqNewer(mConfiguration)
+ && mConfiguration.diff(tmp.createdConfig) != 0)) {
+ if (changedConfig == null
+ || tmp.createdConfig.isOtherSeqNewer(changedConfig)) {
+ changedConfig = tmp.createdConfig;
+ }
+ }
+ }
+
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Relaunching activity "
+ + tmp.token + ": changedConfig=" + changedConfig);
+
+ // If there was a pending configuration change, execute it first.
+ if (changedConfig != null) {
+ mCurDefaultDisplayDpi = changedConfig.densityDpi;
+ updateDefaultDensity();
+ handleConfigurationChanged(changedConfig, null);
+ }
+
+ ActivityClientRecord r = mActivities.get(tmp.token);
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handling relaunch of " + r);
+ if (r == null) {
+ if (!tmp.onlyLocalRequest) {
+ try {
+ ActivityManager.getService().activityRelaunched(tmp.token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return;
+ }
+
+ r.activity.mConfigChangeFlags |= configChanges;
+ r.onlyLocalRequest = tmp.onlyLocalRequest;
+ r.mPreserveWindow = tmp.mPreserveWindow;
+ r.lastProcessedSeq = tmp.lastProcessedSeq;
+ r.relaunchSeq = tmp.relaunchSeq;
+ Intent currentIntent = r.activity.mIntent;
+
+ r.activity.mChangingConfigurations = true;
+
+ // If we are preserving the main window across relaunches we would also like to preserve
+ // the children. However the client side view system does not support preserving
+ // the child views so we notify the window manager to expect these windows to
+ // be replaced and defer requests to destroy or hide them. This way we can achieve
+ // visual continuity. It's important that we do this here prior to pause and destroy
+ // as that is when we may hide or remove the child views.
+ //
+ // There is another scenario, if we have decided locally to relaunch the app from a
+ // call to recreate, then none of the windows will be prepared for replacement or
+ // preserved by the server, so we want to notify it that we are preparing to replace
+ // everything
+ try {
+ if (r.mPreserveWindow || r.onlyLocalRequest) {
+ WindowManagerGlobal.getWindowSession().prepareToReplaceWindows(
+ r.token, !r.onlyLocalRequest);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ // Need to ensure state is saved.
+ if (!r.paused) {
+ performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity");
+ }
+ if (r.state == null && !r.stopped && !r.isPreHoneycomb()) {
+ callCallActivityOnSaveInstanceState(r);
+ }
+
+ handleDestroyActivity(r.token, false, configChanges, true);
+
+ r.activity = null;
+ r.window = null;
+ r.hideForNow = false;
+ r.nextIdle = null;
+ // Merge any pending results and pending intents; don't just replace them
+ if (tmp.pendingResults != null) {
+ if (r.pendingResults == null) {
+ r.pendingResults = tmp.pendingResults;
+ } else {
+ r.pendingResults.addAll(tmp.pendingResults);
+ }
+ }
+ if (tmp.pendingIntents != null) {
+ if (r.pendingIntents == null) {
+ r.pendingIntents = tmp.pendingIntents;
+ } else {
+ r.pendingIntents.addAll(tmp.pendingIntents);
+ }
+ }
+ r.startsNotResumed = tmp.startsNotResumed;
+ r.overrideConfig = tmp.overrideConfig;
+
+ handleLaunchActivity(r, currentIntent, "handleRelaunchActivity");
+
+ if (!tmp.onlyLocalRequest) {
+ try {
+ ActivityManager.getService().activityRelaunched(r.token);
+ if (r.window != null) {
+ r.window.reportActivityRelaunched();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private void callCallActivityOnSaveInstanceState(ActivityClientRecord r) {
+ r.state = new Bundle();
+ r.state.setAllowFds(false);
+ if (r.isPersistable()) {
+ r.persistentState = new PersistableBundle();
+ mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state,
+ r.persistentState);
+ } else {
+ mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
+ }
+ }
+
+ ArrayList<ComponentCallbacks2> collectComponentCallbacks(
+ boolean allActivities, Configuration newConfig) {
+ ArrayList<ComponentCallbacks2> callbacks
+ = new ArrayList<ComponentCallbacks2>();
+
+ synchronized (mResourcesManager) {
+ final int NAPP = mAllApplications.size();
+ for (int i=0; i<NAPP; i++) {
+ callbacks.add(mAllApplications.get(i));
+ }
+ final int NACT = mActivities.size();
+ for (int i=0; i<NACT; i++) {
+ ActivityClientRecord ar = mActivities.valueAt(i);
+ Activity a = ar.activity;
+ if (a != null) {
+ Configuration thisConfig = applyConfigCompatMainThread(
+ mCurDefaultDisplayDpi, newConfig,
+ ar.packageInfo.getCompatibilityInfo());
+ if (!ar.activity.mFinished && (allActivities || !ar.paused)) {
+ // If the activity is currently resumed, its configuration
+ // needs to change right now.
+ callbacks.add(a);
+ } else if (thisConfig != null) {
+ // Otherwise, we will tell it about the change
+ // the next time it is resumed or shown. Note that
+ // the activity manager may, before then, decide the
+ // activity needs to be destroyed to handle its new
+ // configuration.
+ if (DEBUG_CONFIGURATION) {
+ Slog.v(TAG, "Setting activity "
+ + ar.activityInfo.name + " newConfig=" + thisConfig);
+ }
+ ar.newConfig = thisConfig;
+ }
+ }
+ }
+ final int NSVC = mServices.size();
+ for (int i=0; i<NSVC; i++) {
+ callbacks.add(mServices.valueAt(i));
+ }
+ }
+ synchronized (mProviderMap) {
+ final int NPRV = mLocalProviders.size();
+ for (int i=0; i<NPRV; i++) {
+ callbacks.add(mLocalProviders.valueAt(i).mLocalProvider);
+ }
+ }
+
+ return callbacks;
+ }
+
+ /**
+ * Updates the configuration for an Activity. The ActivityClientRecord's
+ * {@link ActivityClientRecord#overrideConfig} is used to compute the final Configuration for
+ * that Activity. {@link ActivityClientRecord#tmpConfig} is used as a temporary for delivering
+ * the updated Configuration.
+ * @param r ActivityClientRecord representing the Activity.
+ * @param newBaseConfig The new configuration to use. This may be augmented with
+ * {@link ActivityClientRecord#overrideConfig}.
+ */
+ private void performConfigurationChangedForActivity(ActivityClientRecord r,
+ Configuration newBaseConfig) {
+ performConfigurationChangedForActivity(r, newBaseConfig,
+ r.activity.getDisplay().getDisplayId(), false /* movedToDifferentDisplay */);
+ }
+
+ /**
+ * Updates the configuration for an Activity. The ActivityClientRecord's
+ * {@link ActivityClientRecord#overrideConfig} is used to compute the final Configuration for
+ * that Activity. {@link ActivityClientRecord#tmpConfig} is used as a temporary for delivering
+ * the updated Configuration.
+ * @param r ActivityClientRecord representing the Activity.
+ * @param newBaseConfig The new configuration to use. This may be augmented with
+ * {@link ActivityClientRecord#overrideConfig}.
+ * @param displayId The id of the display where the Activity currently resides.
+ * @param movedToDifferentDisplay Indicates if the activity was moved to different display.
+ * @return {@link Configuration} instance sent to client, null if not sent.
+ */
+ private Configuration performConfigurationChangedForActivity(ActivityClientRecord r,
+ Configuration newBaseConfig, int displayId, boolean movedToDifferentDisplay) {
+ r.tmpConfig.setTo(newBaseConfig);
+ if (r.overrideConfig != null) {
+ r.tmpConfig.updateFrom(r.overrideConfig);
+ }
+ final Configuration reportedConfig = performActivityConfigurationChanged(r.activity,
+ r.tmpConfig, r.overrideConfig, displayId, movedToDifferentDisplay);
+ freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
+ return reportedConfig;
+ }
+
+ /**
+ * Creates a new Configuration only if override would modify base. Otherwise returns base.
+ * @param base The base configuration.
+ * @param override The update to apply to the base configuration. Can be null.
+ * @return A Configuration representing base with override applied.
+ */
+ private static Configuration createNewConfigAndUpdateIfNotNull(@NonNull Configuration base,
+ @Nullable Configuration override) {
+ if (override == null) {
+ return base;
+ }
+ Configuration newConfig = new Configuration(base);
+ newConfig.updateFrom(override);
+ return newConfig;
+ }
+
+ /**
+ * Decides whether to update a component's configuration and whether to inform it.
+ * @param cb The component callback to notify of configuration change.
+ * @param newConfig The new configuration.
+ */
+ private void performConfigurationChanged(ComponentCallbacks2 cb, Configuration newConfig) {
+ if (!REPORT_TO_ACTIVITY) {
+ return;
+ }
+
+ // ContextThemeWrappers may override the configuration for that context. We must check and
+ // apply any overrides defined.
+ Configuration contextThemeWrapperOverrideConfig = null;
+ if (cb instanceof ContextThemeWrapper) {
+ final ContextThemeWrapper contextThemeWrapper = (ContextThemeWrapper) cb;
+ contextThemeWrapperOverrideConfig = contextThemeWrapper.getOverrideConfiguration();
+ }
+
+ // Apply the ContextThemeWrapper override if necessary.
+ // NOTE: Make sure the configurations are not modified, as they are treated as immutable
+ // in many places.
+ final Configuration configToReport = createNewConfigAndUpdateIfNotNull(
+ newConfig, contextThemeWrapperOverrideConfig);
+ cb.onConfigurationChanged(configToReport);
+ }
+
+ /**
+ * Decides whether to update an Activity's configuration and whether to inform it.
+ * @param activity The activity to notify of configuration change.
+ * @param newConfig The new configuration.
+ * @param amOverrideConfig The override config that differentiates the Activity's configuration
+ * from the base global configuration. This is supplied by
+ * ActivityManager.
+ * @param displayId Id of the display where activity currently resides.
+ * @param movedToDifferentDisplay Indicates if the activity was moved to different display.
+ * @return Configuration sent to client, null if no changes and not moved to different display.
+ */
+ private Configuration performActivityConfigurationChanged(Activity activity,
+ Configuration newConfig, Configuration amOverrideConfig, int displayId,
+ boolean movedToDifferentDisplay) {
+ if (activity == null) {
+ throw new IllegalArgumentException("No activity provided.");
+ }
+ final IBinder activityToken = activity.getActivityToken();
+ if (activityToken == null) {
+ throw new IllegalArgumentException("Activity token not set. Is the activity attached?");
+ }
+
+ boolean shouldChangeConfig = false;
+ if (activity.mCurrentConfig == null) {
+ shouldChangeConfig = true;
+ } else {
+ // If the new config is the same as the config this Activity is already running with and
+ // the override config also didn't change, then don't bother calling
+ // onConfigurationChanged.
+ final int diff = activity.mCurrentConfig.diffPublicOnly(newConfig);
+
+ if (diff != 0 || !mResourcesManager.isSameResourcesOverrideConfig(activityToken,
+ amOverrideConfig)) {
+ // Always send the task-level config changes. For system-level configuration, if
+ // this activity doesn't handle any of the config changes, then don't bother
+ // calling onConfigurationChanged as we're going to destroy it.
+ if (!mUpdatingSystemConfig
+ || (~activity.mActivityInfo.getRealConfigChanged() & diff) == 0
+ || !REPORT_TO_ACTIVITY) {
+ shouldChangeConfig = true;
+ }
+ }
+ }
+ if (!shouldChangeConfig && !movedToDifferentDisplay) {
+ // Nothing significant, don't proceed with updating and reporting.
+ return null;
+ }
+
+ // Propagate the configuration change to ResourcesManager and Activity.
+
+ // ContextThemeWrappers may override the configuration for that context. We must check and
+ // apply any overrides defined.
+ Configuration contextThemeWrapperOverrideConfig = activity.getOverrideConfiguration();
+
+ // We only update an Activity's configuration if this is not a global configuration change.
+ // This must also be done before the callback, or else we violate the contract that the new
+ // resources are available in ComponentCallbacks2#onConfigurationChanged(Configuration).
+ // Also apply the ContextThemeWrapper override if necessary.
+ // NOTE: Make sure the configurations are not modified, as they are treated as immutable in
+ // many places.
+ final Configuration finalOverrideConfig = createNewConfigAndUpdateIfNotNull(
+ amOverrideConfig, contextThemeWrapperOverrideConfig);
+ mResourcesManager.updateResourcesForActivity(activityToken, finalOverrideConfig,
+ displayId, movedToDifferentDisplay);
+
+ activity.mConfigChangeFlags = 0;
+ activity.mCurrentConfig = new Configuration(newConfig);
+
+ // Apply the ContextThemeWrapper override if necessary.
+ // NOTE: Make sure the configurations are not modified, as they are treated as immutable
+ // in many places.
+ final Configuration configToReport = createNewConfigAndUpdateIfNotNull(newConfig,
+ contextThemeWrapperOverrideConfig);
+
+ if (!REPORT_TO_ACTIVITY) {
+ // Not configured to report to activity.
+ return configToReport;
+ }
+
+ if (movedToDifferentDisplay) {
+ activity.dispatchMovedToDisplay(displayId, configToReport);
+ }
+
+ if (shouldChangeConfig) {
+ activity.mCalled = false;
+ activity.onConfigurationChanged(configToReport);
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException("Activity " + activity.getLocalClassName() +
+ " did not call through to super.onConfigurationChanged()");
+ }
+ }
+
+ return configToReport;
+ }
+
+ public final void applyConfigurationToResources(Configuration config) {
+ synchronized (mResourcesManager) {
+ mResourcesManager.applyConfigurationToResourcesLocked(config, null);
+ }
+ }
+
+ final Configuration applyCompatConfiguration(int displayDensity) {
+ Configuration config = mConfiguration;
+ if (mCompatConfiguration == null) {
+ mCompatConfiguration = new Configuration();
+ }
+ mCompatConfiguration.setTo(mConfiguration);
+ if (mResourcesManager.applyCompatConfigurationLocked(displayDensity,
+ mCompatConfiguration)) {
+ config = mCompatConfiguration;
+ }
+ return config;
+ }
+
+ final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {
+
+ int configDiff = 0;
+
+ // This flag tracks whether the new configuration is fundamentally equivalent to the
+ // existing configuration. This is necessary to determine whether non-activity
+ // callbacks should receive notice when the only changes are related to non-public fields.
+ // We do not gate calling {@link #performActivityConfigurationChanged} based on this flag
+ // as that method uses the same check on the activity config override as well.
+ final boolean equivalent = config != null && mConfiguration != null
+ && (0 == mConfiguration.diffPublicOnly(config));
+
+ synchronized (mResourcesManager) {
+ if (mPendingConfiguration != null) {
+ if (!mPendingConfiguration.isOtherSeqNewer(config)) {
+ config = mPendingConfiguration;
+ mCurDefaultDisplayDpi = config.densityDpi;
+ updateDefaultDensity();
+ }
+ mPendingConfiguration = null;
+ }
+
+ if (config == null) {
+ return;
+ }
+
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle configuration changed: "
+ + config);
+
+ mResourcesManager.applyConfigurationToResourcesLocked(config, compat);
+ updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
+ mResourcesManager.getConfiguration().getLocales());
+
+ if (mConfiguration == null) {
+ mConfiguration = new Configuration();
+ }
+ if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
+ return;
+ }
+
+ configDiff = mConfiguration.updateFrom(config);
+ config = applyCompatConfiguration(mCurDefaultDisplayDpi);
+
+ final Theme systemTheme = getSystemContext().getTheme();
+ if ((systemTheme.getChangingConfigurations() & configDiff) != 0) {
+ systemTheme.rebase();
+ }
+
+ final Theme systemUiTheme = getSystemUiContext().getTheme();
+ if ((systemUiTheme.getChangingConfigurations() & configDiff) != 0) {
+ systemUiTheme.rebase();
+ }
+ }
+
+ ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config);
+
+ freeTextLayoutCachesIfNeeded(configDiff);
+
+ if (callbacks != null) {
+ final int N = callbacks.size();
+ for (int i=0; i<N; i++) {
+ ComponentCallbacks2 cb = callbacks.get(i);
+ if (cb instanceof Activity) {
+ // If callback is an Activity - call corresponding method to consider override
+ // config and avoid onConfigurationChanged if it hasn't changed.
+ Activity a = (Activity) cb;
+ performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
+ config);
+ } else if (!equivalent) {
+ performConfigurationChanged(cb, config);
+ }
+ }
+ }
+ }
+
+ void handleApplicationInfoChanged(@NonNull final ApplicationInfo ai) {
+ // Updates triggered by package installation go through a package update
+ // receiver. Here we try to capture ApplicationInfo changes that are
+ // caused by other sources, such as overlays. That means we want to be as conservative
+ // about code changes as possible. Take the diff of the old ApplicationInfo and the new
+ // to see if anything needs to change.
+ LoadedApk apk;
+ LoadedApk resApk;
+ // Update all affected loaded packages with new package information
+ synchronized (mResourcesManager) {
+ WeakReference<LoadedApk> ref = mPackages.get(ai.packageName);
+ apk = ref != null ? ref.get() : null;
+ ref = mResourcePackages.get(ai.packageName);
+ resApk = ref != null ? ref.get() : null;
+ }
+ if (apk != null) {
+ final ArrayList<String> oldPaths = new ArrayList<>();
+ LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths);
+ apk.updateApplicationInfo(ai, oldPaths);
+ }
+ if (resApk != null) {
+ final ArrayList<String> oldPaths = new ArrayList<>();
+ LoadedApk.makePaths(this, resApk.getApplicationInfo(), oldPaths);
+ resApk.updateApplicationInfo(ai, oldPaths);
+ }
+ synchronized (mResourcesManager) {
+ // Update all affected Resources objects to use new ResourcesImpl
+ mResourcesManager.applyNewResourceDirsLocked(ai.sourceDir, ai.resourceDirs);
+ }
+
+ ApplicationPackageManager.configurationChanged();
+
+ // Trigger a regular Configuration change event, only with a different assetsSeq number
+ // so that we actually call through to all components.
+ // TODO(adamlesinski): Change this to make use of ActivityManager's upcoming ability to
+ // store configurations per-process.
+ Configuration newConfig = new Configuration();
+ newConfig.assetsSeq = (mConfiguration != null ? mConfiguration.assetsSeq : 0) + 1;
+ handleConfigurationChanged(newConfig, null);
+
+ requestRelaunchAllActivities();
+ }
+
+ static void freeTextLayoutCachesIfNeeded(int configDiff) {
+ if (configDiff != 0) {
+ // Ask text layout engine to free its caches if there is a locale change
+ boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0);
+ if (hasLocaleConfigChange) {
+ Canvas.freeTextLayoutCaches();
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Cleared TextLayout Caches");
+ }
+ }
+ }
+
+ /**
+ * Handle new activity configuration and/or move to a different display.
+ * @param data Configuration update data.
+ * @param displayId Id of the display where activity was moved to, -1 if there was no move and
+ * value didn't change.
+ */
+ void handleActivityConfigurationChanged(ActivityConfigChangeData data, int displayId) {
+ ActivityClientRecord r = mActivities.get(data.activityToken);
+ // Check input params.
+ if (r == null || r.activity == null) {
+ if (DEBUG_CONFIGURATION) Slog.w(TAG, "Not found target activity to report to: " + r);
+ return;
+ }
+ final boolean movedToDifferentDisplay = displayId != INVALID_DISPLAY
+ && displayId != r.activity.getDisplay().getDisplayId();
+
+ // Perform updates.
+ r.overrideConfig = data.overrideConfig;
+ final ViewRootImpl viewRoot = r.activity.mDecor != null
+ ? r.activity.mDecor.getViewRootImpl() : null;
+
+ if (movedToDifferentDisplay) {
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity moved to display, activity:"
+ + r.activityInfo.name + ", displayId=" + displayId
+ + ", config=" + data.overrideConfig);
+
+ final Configuration reportedConfig = performConfigurationChangedForActivity(r,
+ mCompatConfiguration, displayId, true /* movedToDifferentDisplay */);
+ if (viewRoot != null) {
+ viewRoot.onMovedToDisplay(displayId, reportedConfig);
+ }
+ } else {
+ if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: "
+ + r.activityInfo.name + ", config=" + data.overrideConfig);
+ performConfigurationChangedForActivity(r, mCompatConfiguration);
+ }
+ // Notify the ViewRootImpl instance about configuration changes. It may have initiated this
+ // update to make sure that resources are updated before updating itself.
+ if (viewRoot != null) {
+ viewRoot.updateConfiguration(displayId);
+ }
+ mSomeActivitiesChanged = true;
+ }
+
+ final void handleProfilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) {
+ if (start) {
+ try {
+ switch (profileType) {
+ default:
+ mProfiler.setProfiler(profilerInfo);
+ mProfiler.startProfiling();
+ break;
+ }
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Profiling failed on path " + profilerInfo.profileFile
+ + " -- can the process access this path?");
+ } finally {
+ profilerInfo.closeFd();
+ }
+ } else {
+ switch (profileType) {
+ default:
+ mProfiler.stopProfiling();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Public entrypoint to stop profiling. This is required to end profiling when the app crashes,
+ * so that profiler data won't be lost.
+ *
+ * @hide
+ */
+ public void stopProfiling() {
+ if (mProfiler != null) {
+ mProfiler.stopProfiling();
+ }
+ }
+
+ static void handleDumpHeap(DumpHeapData dhd) {
+ if (dhd.runGc) {
+ System.gc();
+ System.runFinalization();
+ System.gc();
+ }
+ if (dhd.managed) {
+ try {
+ Debug.dumpHprofData(dhd.path, dhd.fd.getFileDescriptor());
+ } catch (IOException e) {
+ Slog.w(TAG, "Managed heap dump failed on path " + dhd.path
+ + " -- can the process access this path?");
+ } finally {
+ try {
+ dhd.fd.close();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failure closing profile fd", e);
+ }
+ }
+ } else if (dhd.mallocInfo) {
+ Debug.dumpNativeMallocInfo(dhd.fd.getFileDescriptor());
+ } else {
+ Debug.dumpNativeHeap(dhd.fd.getFileDescriptor());
+ }
+ try {
+ ActivityManager.getService().dumpHeapFinished(dhd.path);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ final void handleDispatchPackageBroadcast(int cmd, String[] packages) {
+ boolean hasPkgInfo = false;
+ switch (cmd) {
+ case ApplicationThreadConstants.PACKAGE_REMOVED:
+ case ApplicationThreadConstants.PACKAGE_REMOVED_DONT_KILL:
+ {
+ final boolean killApp = cmd == ApplicationThreadConstants.PACKAGE_REMOVED;
+ if (packages == null) {
+ break;
+ }
+ synchronized (mResourcesManager) {
+ for (int i = packages.length - 1; i >= 0; i--) {
+ if (!hasPkgInfo) {
+ WeakReference<LoadedApk> ref = mPackages.get(packages[i]);
+ if (ref != null && ref.get() != null) {
+ hasPkgInfo = true;
+ } else {
+ ref = mResourcePackages.get(packages[i]);
+ if (ref != null && ref.get() != null) {
+ hasPkgInfo = true;
+ }
+ }
+ }
+ if (killApp) {
+ mPackages.remove(packages[i]);
+ mResourcePackages.remove(packages[i]);
+ }
+ }
+ }
+ break;
+ }
+ case ApplicationThreadConstants.PACKAGE_REPLACED:
+ {
+ if (packages == null) {
+ break;
+ }
+ synchronized (mResourcesManager) {
+ for (int i = packages.length - 1; i >= 0; i--) {
+ WeakReference<LoadedApk> ref = mPackages.get(packages[i]);
+ LoadedApk pkgInfo = ref != null ? ref.get() : null;
+ if (pkgInfo != null) {
+ hasPkgInfo = true;
+ } else {
+ ref = mResourcePackages.get(packages[i]);
+ pkgInfo = ref != null ? ref.get() : null;
+ if (pkgInfo != null) {
+ hasPkgInfo = true;
+ }
+ }
+ // If the package is being replaced, yet it still has a valid
+ // LoadedApk object, the package was updated with _DONT_KILL.
+ // Adjust it's internal references to the application info and
+ // resources.
+ if (pkgInfo != null) {
+ try {
+ final String packageName = packages[i];
+ final ApplicationInfo aInfo =
+ sPackageManager.getApplicationInfo(
+ packageName,
+ 0 /*flags*/,
+ UserHandle.myUserId());
+
+ if (mActivities.size() > 0) {
+ for (ActivityClientRecord ar : mActivities.values()) {
+ if (ar.activityInfo.applicationInfo.packageName
+ .equals(packageName)) {
+ ar.activityInfo.applicationInfo = aInfo;
+ ar.packageInfo = pkgInfo;
+ }
+ }
+ }
+ final List<String> oldPaths =
+ sPackageManager.getPreviousCodePaths(packageName);
+ pkgInfo.updateApplicationInfo(aInfo, oldPaths);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ ApplicationPackageManager.handlePackageBroadcast(cmd, packages, hasPkgInfo);
+ }
+
+ final void handleLowMemory() {
+ ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(true, null);
+
+ final int N = callbacks.size();
+ for (int i=0; i<N; i++) {
+ callbacks.get(i).onLowMemory();
+ }
+
+ // Ask SQLite to free up as much memory as it can, mostly from its page caches.
+ if (Process.myUid() != Process.SYSTEM_UID) {
+ int sqliteReleased = SQLiteDatabase.releaseMemory();
+ EventLog.writeEvent(SQLITE_MEM_RELEASED_EVENT_LOG_TAG, sqliteReleased);
+ }
+
+ // Ask graphics to free up as much as possible (font/image caches)
+ Canvas.freeCaches();
+
+ // Ask text layout engine to free also as much as possible
+ Canvas.freeTextLayoutCaches();
+
+ BinderInternal.forceGc("mem");
+ }
+
+ final void handleTrimMemory(int level) {
+ if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Trimming memory to level: " + level);
+
+ ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(true, null);
+
+ final int N = callbacks.size();
+ for (int i = 0; i < N; i++) {
+ callbacks.get(i).onTrimMemory(level);
+ }
+
+ WindowManagerGlobal.getInstance().trimMemory(level);
+ }
+
+ private void setupGraphicsSupport(Context context) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setupGraphicsSupport");
+
+ // The system package doesn't have real data directories, so don't set up cache paths.
+ if (!"android".equals(context.getPackageName())) {
+ // This cache location probably points at credential-encrypted
+ // storage which may not be accessible yet; assign it anyway instead
+ // of pointing at device-encrypted storage.
+ final File cacheDir = context.getCacheDir();
+ if (cacheDir != null) {
+ // Provide a usable directory for temporary files
+ System.setProperty("java.io.tmpdir", cacheDir.getAbsolutePath());
+ } else {
+ Log.v(TAG, "Unable to initialize \"java.io.tmpdir\" property "
+ + "due to missing cache directory");
+ }
+
+ // Setup a location to store generated/compiled graphics code.
+ final Context deviceContext = context.createDeviceProtectedStorageContext();
+ final File codeCacheDir = deviceContext.getCodeCacheDir();
+ if (codeCacheDir != null) {
+ try {
+ int uid = Process.myUid();
+ String[] packages = getPackageManager().getPackagesForUid(uid);
+ if (packages != null) {
+ ThreadedRenderer.setupDiskCache(codeCacheDir);
+ RenderScriptCacheDir.setupDiskCache(codeCacheDir);
+ }
+ } catch (RemoteException e) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ Log.w(TAG, "Unable to use shader/script cache: missing code-cache directory");
+ }
+ }
+
+ GraphicsEnvironment.chooseDriver(context);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+
+ private void updateDefaultDensity() {
+ final int densityDpi = mCurDefaultDisplayDpi;
+ if (!mDensityCompatMode
+ && densityDpi != Configuration.DENSITY_DPI_UNDEFINED
+ && densityDpi != DisplayMetrics.DENSITY_DEVICE) {
+ DisplayMetrics.DENSITY_DEVICE = densityDpi;
+ Bitmap.setDefaultDensity(densityDpi);
+ }
+ }
+
+ /**
+ * Returns the correct library directory for the current ABI.
+ * <p>
+ * If we're dealing with a multi-arch application that has both 32 and 64 bit shared
+ * libraries, we might need to choose the secondary depending on what the current
+ * runtime's instruction set is.
+ */
+ private String getInstrumentationLibrary(ApplicationInfo appInfo, InstrumentationInfo insInfo) {
+ if (appInfo.primaryCpuAbi != null && appInfo.secondaryCpuAbi != null) {
+ // Get the instruction set supported by the secondary ABI. In the presence
+ // of a native bridge this might be different than the one secondary ABI used.
+ String secondaryIsa =
+ VMRuntime.getInstructionSet(appInfo.secondaryCpuAbi);
+ final String secondaryDexCodeIsa =
+ SystemProperties.get("ro.dalvik.vm.isa." + secondaryIsa);
+ secondaryIsa = secondaryDexCodeIsa.isEmpty() ? secondaryIsa : secondaryDexCodeIsa;
+
+ final String runtimeIsa = VMRuntime.getRuntime().vmInstructionSet();
+ if (runtimeIsa.equals(secondaryIsa)) {
+ return insInfo.secondaryNativeLibraryDir;
+ }
+ }
+ return insInfo.nativeLibraryDir;
+ }
+
+ /**
+ * The LocaleList set for the app's resources may have been shuffled so that the preferred
+ * Locale is at position 0. We must find the index of this preferred Locale in the
+ * original LocaleList.
+ */
+ private void updateLocaleListFromAppContext(Context context, LocaleList newLocaleList) {
+ final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0);
+ final int newLocaleListSize = newLocaleList.size();
+ for (int i = 0; i < newLocaleListSize; i++) {
+ if (bestLocale.equals(newLocaleList.get(i))) {
+ LocaleList.setDefault(newLocaleList, i);
+ return;
+ }
+ }
+
+ // The app may have overridden the LocaleList with its own Locale
+ // (not present in the available list). Push the chosen Locale
+ // to the front of the list.
+ LocaleList.setDefault(new LocaleList(bestLocale, newLocaleList));
+ }
+
+ private void handleBindApplication(AppBindData data) {
+ // Register the UI Thread as a sensitive thread to the runtime.
+ VMRuntime.registerSensitiveThread();
+ if (data.trackAllocation) {
+ DdmVmInternal.enableRecentAllocations(true);
+ }
+
+ // Note when this process has started.
+ Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());
+
+ mBoundApplication = data;
+ mConfiguration = new Configuration(data.config);
+ mCompatConfiguration = new Configuration(data.config);
+
+ mProfiler = new Profiler();
+ if (data.initProfilerInfo != null) {
+ mProfiler.profileFile = data.initProfilerInfo.profileFile;
+ mProfiler.profileFd = data.initProfilerInfo.profileFd;
+ mProfiler.samplingInterval = data.initProfilerInfo.samplingInterval;
+ mProfiler.autoStopProfiler = data.initProfilerInfo.autoStopProfiler;
+ mProfiler.streamingOutput = data.initProfilerInfo.streamingOutput;
+ }
+
+ // send up app name; do this *before* waiting for debugger
+ Process.setArgV0(data.processName);
+ android.ddm.DdmHandleAppName.setAppName(data.processName,
+ UserHandle.myUserId());
+
+ if (mProfiler.profileFd != null) {
+ mProfiler.startProfiling();
+ }
+
+ // If the app is Honeycomb MR1 or earlier, switch its AsyncTask
+ // implementation to use the pool executor. Normally, we use the
+ // serialized executor as the default. This has to happen in the
+ // main thread so the main looper is set right.
+ if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
+ AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ Message.updateCheckRecycle(data.appInfo.targetSdkVersion);
+
+ /*
+ * Before spawning a new process, reset the time zone to be the system time zone.
+ * This needs to be done because the system time zone could have changed after the
+ * the spawning of this process. Without doing this this process would have the incorrect
+ * system time zone.
+ */
+ TimeZone.setDefault(null);
+
+ /*
+ * Set the LocaleList. This may change once we create the App Context.
+ */
+ LocaleList.setDefault(data.config.getLocales());
+
+ synchronized (mResourcesManager) {
+ /*
+ * Update the system configuration since its preloaded and might not
+ * reflect configuration changes. The configuration object passed
+ * in AppBindData can be safely assumed to be up to date
+ */
+ mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo);
+ mCurDefaultDisplayDpi = data.config.densityDpi;
+
+ // This calls mResourcesManager so keep it within the synchronized block.
+ applyCompatConfiguration(mCurDefaultDisplayDpi);
+ }
+
+ data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
+
+ /**
+ * Switch this process to density compatibility mode if needed.
+ */
+ if ((data.appInfo.flags&ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES)
+ == 0) {
+ mDensityCompatMode = true;
+ Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEFAULT);
+ }
+ updateDefaultDensity();
+
+ final String use24HourSetting = mCoreSettings.getString(Settings.System.TIME_12_24);
+ Boolean is24Hr = null;
+ if (use24HourSetting != null) {
+ is24Hr = "24".equals(use24HourSetting) ? Boolean.TRUE : Boolean.FALSE;
+ }
+ // null : use locale default for 12/24 hour formatting,
+ // false : use 12 hour format,
+ // true : use 24 hour format.
+ DateFormat.set24HourTimePref(is24Hr);
+
+ View.mDebugViewAttributes =
+ mCoreSettings.getInt(Settings.Global.DEBUG_VIEW_ATTRIBUTES, 0) != 0;
+
+ /**
+ * For system applications on userdebug/eng builds, log stack
+ * traces of disk and network access to dropbox for analysis.
+ */
+ if ((data.appInfo.flags &
+ (ApplicationInfo.FLAG_SYSTEM |
+ ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) {
+ StrictMode.conditionallyEnableDebugLogging();
+ }
+
+ /**
+ * For apps targetting Honeycomb or later, we don't allow network usage
+ * on the main event loop / UI thread. This is what ultimately throws
+ * {@link NetworkOnMainThreadException}.
+ */
+ if (data.appInfo.targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
+ StrictMode.enableDeathOnNetwork();
+ }
+
+ /**
+ * For apps targetting N or later, we don't allow file:// Uri exposure.
+ * This is what ultimately throws {@link FileUriExposedException}.
+ */
+ if (data.appInfo.targetSdkVersion >= Build.VERSION_CODES.N) {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+
+ // We deprecated Build.SERIAL and only apps that target pre NMR1
+ // SDK can see it. Since access to the serial is now behind a
+ // permission we push down the value and here we fix it up
+ // before any app code has been loaded.
+ try {
+ Field field = Build.class.getDeclaredField("SERIAL");
+ field.setAccessible(true);
+ field.set(Build.class, data.buildSerial);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ /* ignore */
+ }
+
+ if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) {
+ // XXX should have option to change the port.
+ Debug.changeDebugPort(8100);
+ if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) {
+ Slog.w(TAG, "Application " + data.info.getPackageName()
+ + " is waiting for the debugger on port 8100...");
+
+ IActivityManager mgr = ActivityManager.getService();
+ try {
+ mgr.showWaitingForDebugger(mAppThread, true);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
+ Debug.waitForDebugger();
+
+ try {
+ mgr.showWaitingForDebugger(mAppThread, false);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
+ } else {
+ Slog.w(TAG, "Application " + data.info.getPackageName()
+ + " can be debugged on port 8100...");
+ }
+ }
+
+ // Allow application-generated systrace messages if we're debuggable.
+ boolean isAppDebuggable = (data.appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+ Trace.setAppTracingAllowed(isAppDebuggable);
+ if (isAppDebuggable && data.enableBinderTracking) {
+ Binder.enableTracing();
+ }
+
+ /**
+ * Initialize the default http proxy in this process for the reasons we set the time zone.
+ */
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Setup proxies");
+ final IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
+ if (b != null) {
+ // In pre-boot mode (doing initial launch to collect password), not
+ // all system is up. This includes the connectivity service, so don't
+ // crash if we can't get it.
+ final IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);
+ try {
+ final ProxyInfo proxyInfo = service.getProxyForNetwork(null);
+ Proxy.setHttpProxySystemProperty(proxyInfo);
+ } catch (RemoteException e) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ // Instrumentation info affects the class loader, so load it before
+ // setting up the app context.
+ final InstrumentationInfo ii;
+ if (data.instrumentationName != null) {
+ try {
+ ii = new ApplicationPackageManager(null, getPackageManager())
+ .getInstrumentationInfo(data.instrumentationName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(
+ "Unable to find instrumentation info for: " + data.instrumentationName);
+ }
+
+ mInstrumentationPackageName = ii.packageName;
+ mInstrumentationAppDir = ii.sourceDir;
+ mInstrumentationSplitAppDirs = ii.splitSourceDirs;
+ mInstrumentationLibDir = getInstrumentationLibrary(data.appInfo, ii);
+ mInstrumentedAppDir = data.info.getAppDir();
+ mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();
+ mInstrumentedLibDir = data.info.getLibDir();
+ } else {
+ ii = null;
+ }
+
+ final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
+ updateLocaleListFromAppContext(appContext,
+ mResourcesManager.getConfiguration().getLocales());
+
+ if (!Process.isIsolated()) {
+ setupGraphicsSupport(appContext);
+ }
+
+ // If we use profiles, setup the dex reporter to notify package manager
+ // of any relevant dex loads. The idle maintenance job will use the information
+ // reported to optimize the loaded dex files.
+ // Note that we only need one global reporter per app.
+ // Make sure we do this before calling onCreate so that we can capture the
+ // complete application startup.
+ if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
+ BaseDexClassLoader.setReporter(DexLoadReporter.getInstance());
+ }
+
+ // Install the Network Security Config Provider. This must happen before the application
+ // code is loaded to prevent issues with instances of TLS objects being created before
+ // the provider is installed.
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "NetworkSecurityConfigProvider.install");
+ NetworkSecurityConfigProvider.install(appContext);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ // Continue loading instrumentation.
+ if (ii != null) {
+ final ApplicationInfo instrApp = new ApplicationInfo();
+ ii.copyTo(instrApp);
+ instrApp.initForUser(UserHandle.myUserId());
+ final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
+ appContext.getClassLoader(), false, true, false);
+ final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
+
+ try {
+ final ClassLoader cl = instrContext.getClassLoader();
+ mInstrumentation = (Instrumentation)
+ cl.loadClass(data.instrumentationName.getClassName()).newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unable to instantiate instrumentation "
+ + data.instrumentationName + ": " + e.toString(), e);
+ }
+
+ final ComponentName component = new ComponentName(ii.packageName, ii.name);
+ mInstrumentation.init(this, instrContext, appContext, component,
+ data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
+
+ if (mProfiler.profileFile != null && !ii.handleProfiling
+ && mProfiler.profileFd == null) {
+ mProfiler.handlingProfiling = true;
+ final File file = new File(mProfiler.profileFile);
+ file.getParentFile().mkdirs();
+ Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
+ }
+ } else {
+ mInstrumentation = new Instrumentation();
+ }
+
+ if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) {
+ dalvik.system.VMRuntime.getRuntime().clearGrowthLimit();
+ } else {
+ // Small heap, clamp to the current growth limit and let the heap release
+ // pages after the growth limit to the non growth limit capacity. b/18387825
+ dalvik.system.VMRuntime.getRuntime().clampGrowthLimit();
+ }
+
+ // Allow disk access during application and provider setup. This could
+ // block processing ordered broadcasts, but later processing would
+ // probably end up doing the same disk access.
+ Application app;
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
+ final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
+ try {
+ // If the app is being launched for full backup or restore, bring it up in
+ // a restricted environment with the base application class.
+ app = data.info.makeApplication(data.restrictedBackupMode, null);
+ mInitialApplication = app;
+
+ // don't bring up providers in restricted mode; they may depend on the
+ // app's custom Application class
+ if (!data.restrictedBackupMode) {
+ if (!ArrayUtils.isEmpty(data.providers)) {
+ installContentProviders(app, data.providers);
+ // For process that contains content providers, we want to
+ // ensure that the JIT is enabled "at some point".
+ mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
+ }
+ }
+
+ // Do this after providers, since instrumentation tests generally start their
+ // test thread at this point, and we don't want that racing.
+ try {
+ mInstrumentation.onCreate(data.instrumentationArgs);
+ }
+ catch (Exception e) {
+ throw new RuntimeException(
+ "Exception thrown in onCreate() of "
+ + data.instrumentationName + ": " + e.toString(), e);
+ }
+ try {
+ mInstrumentation.callApplicationOnCreate(app);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(app, e)) {
+ throw new RuntimeException(
+ "Unable to create application " + app.getClass().getName()
+ + ": " + e.toString(), e);
+ }
+ }
+ } finally {
+ // If the app targets < O-MR1, or doesn't change the thread policy
+ // during startup, clobber the policy to maintain behavior of b/36951662
+ if (data.appInfo.targetSdkVersion <= Build.VERSION_CODES.O
+ || StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+
+ // Preload fonts resources
+ FontsContract.setApplicationContextForResources(appContext);
+ try {
+ final ApplicationInfo info =
+ getPackageManager().getApplicationInfo(
+ data.appInfo.packageName,
+ PackageManager.GET_META_DATA /*flags*/,
+ UserHandle.myUserId());
+ if (info.metaData != null) {
+ final int preloadedFontsResource = info.metaData.getInt(
+ ApplicationInfo.METADATA_PRELOADED_FONTS, 0);
+ if (preloadedFontsResource != 0) {
+ data.info.mResources.preloadFonts(preloadedFontsResource);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /*package*/ final void finishInstrumentation(int resultCode, Bundle results) {
+ IActivityManager am = ActivityManager.getService();
+ if (mProfiler.profileFile != null && mProfiler.handlingProfiling
+ && mProfiler.profileFd == null) {
+ Debug.stopMethodTracing();
+ }
+ //Slog.i(TAG, "am: " + ActivityManager.getService()
+ // + ", app thr: " + mAppThread);
+ try {
+ am.finishInstrumentation(mAppThread, resultCode, results);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ private void installContentProviders(
+ Context context, List<ProviderInfo> providers) {
+ final ArrayList<ContentProviderHolder> results = new ArrayList<>();
+
+ for (ProviderInfo cpi : providers) {
+ if (DEBUG_PROVIDER) {
+ StringBuilder buf = new StringBuilder(128);
+ buf.append("Pub ");
+ buf.append(cpi.authority);
+ buf.append(": ");
+ buf.append(cpi.name);
+ Log.i(TAG, buf.toString());
+ }
+ ContentProviderHolder cph = installProvider(context, null, cpi,
+ false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
+ if (cph != null) {
+ cph.noReleaseNeeded = true;
+ results.add(cph);
+ }
+ }
+
+ try {
+ ActivityManager.getService().publishContentProviders(
+ getApplicationThread(), results);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ public final IContentProvider acquireProvider(
+ Context c, String auth, int userId, boolean stable) {
+ final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
+ if (provider != null) {
+ return provider;
+ }
+
+ // There is a possible race here. Another thread may try to acquire
+ // the same provider at the same time. When this happens, we want to ensure
+ // that the first one wins.
+ // Note that we cannot hold the lock while acquiring and installing the
+ // provider since it might take a long time to run and it could also potentially
+ // be re-entrant in the case where the provider is in the same process.
+ ContentProviderHolder holder = null;
+ try {
+ holder = ActivityManager.getService().getContentProvider(
+ getApplicationThread(), auth, userId, stable);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ if (holder == null) {
+ Slog.e(TAG, "Failed to find provider info for " + auth);
+ return null;
+ }
+
+ // Install provider will increment the reference count for us, and break
+ // any ties in the race.
+ holder = installProvider(c, holder, holder.info,
+ true /*noisy*/, holder.noReleaseNeeded, stable);
+ return holder.provider;
+ }
+
+ private final void incProviderRefLocked(ProviderRefCount prc, boolean stable) {
+ if (stable) {
+ prc.stableCount += 1;
+ if (prc.stableCount == 1) {
+ // We are acquiring a new stable reference on the provider.
+ int unstableDelta;
+ if (prc.removePending) {
+ // We have a pending remove operation, which is holding the
+ // last unstable reference. At this point we are converting
+ // that unstable reference to our new stable reference.
+ unstableDelta = -1;
+ // Cancel the removal of the provider.
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "incProviderRef: stable "
+ + "snatched provider from the jaws of death");
+ }
+ prc.removePending = false;
+ // There is a race! It fails to remove the message, which
+ // will be handled in completeRemoveProvider().
+ mH.removeMessages(H.REMOVE_PROVIDER, prc);
+ } else {
+ unstableDelta = 0;
+ }
+ try {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "incProviderRef Now stable - "
+ + prc.holder.info.name + ": unstableDelta="
+ + unstableDelta);
+ }
+ ActivityManager.getService().refContentProvider(
+ prc.holder.connection, 1, unstableDelta);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ } else {
+ prc.unstableCount += 1;
+ if (prc.unstableCount == 1) {
+ // We are acquiring a new unstable reference on the provider.
+ if (prc.removePending) {
+ // Oh look, we actually have a remove pending for the
+ // provider, which is still holding the last unstable
+ // reference. We just need to cancel that to take new
+ // ownership of the reference.
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "incProviderRef: unstable "
+ + "snatched provider from the jaws of death");
+ }
+ prc.removePending = false;
+ mH.removeMessages(H.REMOVE_PROVIDER, prc);
+ } else {
+ // First unstable ref, increment our count in the
+ // activity manager.
+ try {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "incProviderRef: Now unstable - "
+ + prc.holder.info.name);
+ }
+ ActivityManager.getService().refContentProvider(
+ prc.holder.connection, 0, 1);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ }
+ }
+ }
+
+ public final IContentProvider acquireExistingProvider(
+ Context c, String auth, int userId, boolean stable) {
+ synchronized (mProviderMap) {
+ final ProviderKey key = new ProviderKey(auth, userId);
+ final ProviderClientRecord pr = mProviderMap.get(key);
+ if (pr == null) {
+ return null;
+ }
+
+ IContentProvider provider = pr.mProvider;
+ IBinder jBinder = provider.asBinder();
+ if (!jBinder.isBinderAlive()) {
+ // The hosting process of the provider has died; we can't
+ // use this one.
+ Log.i(TAG, "Acquiring provider " + auth + " for user " + userId
+ + ": existing object's process dead");
+ handleUnstableProviderDiedLocked(jBinder, true);
+ return null;
+ }
+
+ // Only increment the ref count if we have one. If we don't then the
+ // provider is not reference counted and never needs to be released.
+ ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
+ if (prc != null) {
+ incProviderRefLocked(prc, stable);
+ }
+ return provider;
+ }
+ }
+
+ public final boolean releaseProvider(IContentProvider provider, boolean stable) {
+ if (provider == null) {
+ return false;
+ }
+
+ IBinder jBinder = provider.asBinder();
+ synchronized (mProviderMap) {
+ ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
+ if (prc == null) {
+ // The provider has no ref count, no release is needed.
+ return false;
+ }
+
+ boolean lastRef = false;
+ if (stable) {
+ if (prc.stableCount == 0) {
+ if (DEBUG_PROVIDER) Slog.v(TAG,
+ "releaseProvider: stable ref count already 0, how?");
+ return false;
+ }
+ prc.stableCount -= 1;
+ if (prc.stableCount == 0) {
+ // What we do at this point depends on whether there are
+ // any unstable refs left: if there are, we just tell the
+ // activity manager to decrement its stable count; if there
+ // aren't, we need to enqueue this provider to be removed,
+ // and convert to holding a single unstable ref while
+ // doing so.
+ lastRef = prc.unstableCount == 0;
+ try {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "releaseProvider: No longer stable w/lastRef="
+ + lastRef + " - " + prc.holder.info.name);
+ }
+ ActivityManager.getService().refContentProvider(
+ prc.holder.connection, -1, lastRef ? 1 : 0);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ } else {
+ if (prc.unstableCount == 0) {
+ if (DEBUG_PROVIDER) Slog.v(TAG,
+ "releaseProvider: unstable ref count already 0, how?");
+ return false;
+ }
+ prc.unstableCount -= 1;
+ if (prc.unstableCount == 0) {
+ // If this is the last reference, we need to enqueue
+ // this provider to be removed instead of telling the
+ // activity manager to remove it at this point.
+ lastRef = prc.stableCount == 0;
+ if (!lastRef) {
+ try {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "releaseProvider: No longer unstable - "
+ + prc.holder.info.name);
+ }
+ ActivityManager.getService().refContentProvider(
+ prc.holder.connection, 0, -1);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ }
+ }
+
+ if (lastRef) {
+ if (!prc.removePending) {
+ // Schedule the actual remove asynchronously, since we don't know the context
+ // this will be called in.
+ // TODO: it would be nice to post a delayed message, so
+ // if we come back and need the same provider quickly
+ // we will still have it available.
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "releaseProvider: Enqueueing pending removal - "
+ + prc.holder.info.name);
+ }
+ prc.removePending = true;
+ Message msg = mH.obtainMessage(H.REMOVE_PROVIDER, prc);
+ mH.sendMessage(msg);
+ } else {
+ Slog.w(TAG, "Duplicate remove pending of provider " + prc.holder.info.name);
+ }
+ }
+ return true;
+ }
+ }
+
+ final void completeRemoveProvider(ProviderRefCount prc) {
+ synchronized (mProviderMap) {
+ if (!prc.removePending) {
+ // There was a race! Some other client managed to acquire
+ // the provider before the removal was completed.
+ // Abort the removal. We will do it later.
+ if (DEBUG_PROVIDER) Slog.v(TAG, "completeRemoveProvider: lost the race, "
+ + "provider still in use");
+ return;
+ }
+
+ // More complicated race!! Some client managed to acquire the
+ // provider and release it before the removal was completed.
+ // Continue the removal, and abort the next remove message.
+ prc.removePending = false;
+
+ final IBinder jBinder = prc.holder.provider.asBinder();
+ ProviderRefCount existingPrc = mProviderRefCountMap.get(jBinder);
+ if (existingPrc == prc) {
+ mProviderRefCountMap.remove(jBinder);
+ }
+
+ for (int i=mProviderMap.size()-1; i>=0; i--) {
+ ProviderClientRecord pr = mProviderMap.valueAt(i);
+ IBinder myBinder = pr.mProvider.asBinder();
+ if (myBinder == jBinder) {
+ mProviderMap.removeAt(i);
+ }
+ }
+ }
+
+ try {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "removeProvider: Invoking ActivityManagerService."
+ + "removeContentProvider(" + prc.holder.info.name + ")");
+ }
+ ActivityManager.getService().removeContentProvider(
+ prc.holder.connection, false);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+
+ final void handleUnstableProviderDied(IBinder provider, boolean fromClient) {
+ synchronized (mProviderMap) {
+ handleUnstableProviderDiedLocked(provider, fromClient);
+ }
+ }
+
+ final void handleUnstableProviderDiedLocked(IBinder provider, boolean fromClient) {
+ ProviderRefCount prc = mProviderRefCountMap.get(provider);
+ if (prc != null) {
+ if (DEBUG_PROVIDER) Slog.v(TAG, "Cleaning up dead provider "
+ + provider + " " + prc.holder.info.name);
+ mProviderRefCountMap.remove(provider);
+ for (int i=mProviderMap.size()-1; i>=0; i--) {
+ ProviderClientRecord pr = mProviderMap.valueAt(i);
+ if (pr != null && pr.mProvider.asBinder() == provider) {
+ Slog.i(TAG, "Removing dead content provider:" + pr.mProvider.toString());
+ mProviderMap.removeAt(i);
+ }
+ }
+
+ if (fromClient) {
+ // We found out about this due to execution in our client
+ // code. Tell the activity manager about it now, to ensure
+ // that the next time we go to do anything with the provider
+ // it knows it is dead (so we don't race with its death
+ // notification).
+ try {
+ ActivityManager.getService().unstableProviderDied(
+ prc.holder.connection);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ }
+ }
+
+ final void appNotRespondingViaProvider(IBinder provider) {
+ synchronized (mProviderMap) {
+ ProviderRefCount prc = mProviderRefCountMap.get(provider);
+ if (prc != null) {
+ try {
+ ActivityManager.getService()
+ .appNotRespondingViaProvider(prc.holder.connection);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
+ ContentProvider localProvider, ContentProviderHolder holder) {
+ final String auths[] = holder.info.authority.split(";");
+ final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);
+
+ if (provider != null) {
+ // If this provider is hosted by the core OS and cannot be upgraded,
+ // then I guess we're okay doing blocking calls to it.
+ for (String auth : auths) {
+ switch (auth) {
+ case ContactsContract.AUTHORITY:
+ case CallLog.AUTHORITY:
+ case CallLog.SHADOW_AUTHORITY:
+ case BlockedNumberContract.AUTHORITY:
+ case CalendarContract.AUTHORITY:
+ case Downloads.Impl.AUTHORITY:
+ case "telephony":
+ Binder.allowBlocking(provider.asBinder());
+ }
+ }
+ }
+
+ final ProviderClientRecord pcr = new ProviderClientRecord(
+ auths, provider, localProvider, holder);
+ for (String auth : auths) {
+ final ProviderKey key = new ProviderKey(auth, userId);
+ final ProviderClientRecord existing = mProviderMap.get(key);
+ if (existing != null) {
+ Slog.w(TAG, "Content provider " + pcr.mHolder.info.name
+ + " already published as " + auth);
+ } else {
+ mProviderMap.put(key, pcr);
+ }
+ }
+ return pcr;
+ }
+
+ /**
+ * Installs the provider.
+ *
+ * Providers that are local to the process or that come from the system server
+ * may be installed permanently which is indicated by setting noReleaseNeeded to true.
+ * Other remote providers are reference counted. The initial reference count
+ * for all reference counted providers is one. Providers that are not reference
+ * counted do not have a reference count (at all).
+ *
+ * This method detects when a provider has already been installed. When this happens,
+ * it increments the reference count of the existing provider (if appropriate)
+ * and returns the existing provider. This can happen due to concurrent
+ * attempts to acquire the same provider.
+ */
+ private ContentProviderHolder installProvider(Context context,
+ ContentProviderHolder holder, ProviderInfo info,
+ boolean noisy, boolean noReleaseNeeded, boolean stable) {
+ ContentProvider localProvider = null;
+ IContentProvider provider;
+ if (holder == null || holder.provider == null) {
+ if (DEBUG_PROVIDER || noisy) {
+ Slog.d(TAG, "Loading provider " + info.authority + ": "
+ + info.name);
+ }
+ Context c = null;
+ ApplicationInfo ai = info.applicationInfo;
+ if (context.getPackageName().equals(ai.packageName)) {
+ c = context;
+ } else if (mInitialApplication != null &&
+ mInitialApplication.getPackageName().equals(ai.packageName)) {
+ c = mInitialApplication;
+ } else {
+ try {
+ c = context.createPackageContext(ai.packageName,
+ Context.CONTEXT_INCLUDE_CODE);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore
+ }
+ }
+ if (c == null) {
+ Slog.w(TAG, "Unable to get context for package " +
+ ai.packageName +
+ " while loading content provider " +
+ info.name);
+ return null;
+ }
+
+ if (info.splitName != null) {
+ try {
+ c = c.createContextForSplit(info.splitName);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ try {
+ final java.lang.ClassLoader cl = c.getClassLoader();
+ localProvider = (ContentProvider)cl.
+ loadClass(info.name).newInstance();
+ provider = localProvider.getIContentProvider();
+ if (provider == null) {
+ Slog.e(TAG, "Failed to instantiate class " +
+ info.name + " from sourceDir " +
+ info.applicationInfo.sourceDir);
+ return null;
+ }
+ if (DEBUG_PROVIDER) Slog.v(
+ TAG, "Instantiating local provider " + info.name);
+ // XXX Need to create the correct context for this provider.
+ localProvider.attachInfo(c, info);
+ } catch (java.lang.Exception e) {
+ if (!mInstrumentation.onException(null, e)) {
+ throw new RuntimeException(
+ "Unable to get provider " + info.name
+ + ": " + e.toString(), e);
+ }
+ return null;
+ }
+ } else {
+ provider = holder.provider;
+ if (DEBUG_PROVIDER) Slog.v(TAG, "Installing external provider " + info.authority + ": "
+ + info.name);
+ }
+
+ ContentProviderHolder retHolder;
+
+ synchronized (mProviderMap) {
+ if (DEBUG_PROVIDER) Slog.v(TAG, "Checking to add " + provider
+ + " / " + info.name);
+ IBinder jBinder = provider.asBinder();
+ if (localProvider != null) {
+ ComponentName cname = new ComponentName(info.packageName, info.name);
+ ProviderClientRecord pr = mLocalProvidersByName.get(cname);
+ if (pr != null) {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "installProvider: lost the race, "
+ + "using existing local provider");
+ }
+ provider = pr.mProvider;
+ } else {
+ holder = new ContentProviderHolder(info);
+ holder.provider = provider;
+ holder.noReleaseNeeded = true;
+ pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
+ mLocalProviders.put(jBinder, pr);
+ mLocalProvidersByName.put(cname, pr);
+ }
+ retHolder = pr.mHolder;
+ } else {
+ ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
+ if (prc != null) {
+ if (DEBUG_PROVIDER) {
+ Slog.v(TAG, "installProvider: lost the race, updating ref count");
+ }
+ // We need to transfer our new reference to the existing
+ // ref count, releasing the old one... but only if
+ // release is needed (that is, it is not running in the
+ // system process).
+ if (!noReleaseNeeded) {
+ incProviderRefLocked(prc, stable);
+ try {
+ ActivityManager.getService().removeContentProvider(
+ holder.connection, stable);
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ }
+ }
+ } else {
+ ProviderClientRecord client = installProviderAuthoritiesLocked(
+ provider, localProvider, holder);
+ if (noReleaseNeeded) {
+ prc = new ProviderRefCount(holder, client, 1000, 1000);
+ } else {
+ prc = stable
+ ? new ProviderRefCount(holder, client, 1, 0)
+ : new ProviderRefCount(holder, client, 0, 1);
+ }
+ mProviderRefCountMap.put(jBinder, prc);
+ }
+ retHolder = prc.holder;
+ }
+ }
+ return retHolder;
+ }
+
+ private void handleRunIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) {
+ try {
+ Method main = Class.forName(entryPoint).getMethod("main", String[].class);
+ main.invoke(null, new Object[]{entryPointArgs});
+ } catch (ReflectiveOperationException e) {
+ throw new AndroidRuntimeException("runIsolatedEntryPoint failed", e);
+ }
+ // The process will be empty after this method returns; exit the VM now.
+ System.exit(0);
+ }
+
+ private void attach(boolean system) {
+ sCurrentActivityThread = this;
+ mSystemThread = system;
+ if (!system) {
+ ViewRootImpl.addFirstDrawHandler(new Runnable() {
+ @Override
+ public void run() {
+ ensureJitEnabled();
+ }
+ });
+ android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
+ UserHandle.myUserId());
+ RuntimeInit.setApplicationObject(mAppThread.asBinder());
+ final IActivityManager mgr = ActivityManager.getService();
+ try {
+ mgr.attachApplication(mAppThread);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ // Watch for getting close to heap limit.
+ BinderInternal.addGcWatcher(new Runnable() {
+ @Override public void run() {
+ if (!mSomeActivitiesChanged) {
+ return;
+ }
+ Runtime runtime = Runtime.getRuntime();
+ long dalvikMax = runtime.maxMemory();
+ long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
+ if (dalvikUsed > ((3*dalvikMax)/4)) {
+ if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)
+ + " total=" + (runtime.totalMemory()/1024)
+ + " used=" + (dalvikUsed/1024));
+ mSomeActivitiesChanged = false;
+ try {
+ mgr.releaseSomeActivities(mAppThread);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ });
+ } else {
+ // Don't set application object here -- if the system crashes,
+ // we can't display an alert, we just want to die die die.
+ android.ddm.DdmHandleAppName.setAppName("system_process",
+ UserHandle.myUserId());
+ try {
+ mInstrumentation = new Instrumentation();
+ ContextImpl context = ContextImpl.createAppContext(
+ this, getSystemContext().mPackageInfo);
+ mInitialApplication = context.mPackageInfo.makeApplication(true, null);
+ mInitialApplication.onCreate();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unable to instantiate Application():" + e.toString(), e);
+ }
+ }
+
+ // add dropbox logging to libcore
+ DropBox.setReporter(new DropBoxReporter());
+
+ ViewRootImpl.ConfigChangedCallback configChangedCallback
+ = (Configuration globalConfig) -> {
+ synchronized (mResourcesManager) {
+ // We need to apply this change to the resources immediately, because upon returning
+ // the view hierarchy will be informed about it.
+ if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
+ null /* compat */)) {
+ updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
+ mResourcesManager.getConfiguration().getLocales());
+
+ // This actually changed the resources! Tell everyone about it.
+ if (mPendingConfiguration == null
+ || mPendingConfiguration.isOtherSeqNewer(globalConfig)) {
+ mPendingConfiguration = globalConfig;
+ sendMessage(H.CONFIGURATION_CHANGED, globalConfig);
+ }
+ }
+ }
+ };
+ ViewRootImpl.addConfigCallback(configChangedCallback);
+ }
+
+ public static ActivityThread systemMain() {
+ // The system process on low-memory devices do not get to use hardware
+ // accelerated drawing, since this can add too much overhead to the
+ // process.
+ if (!ActivityManager.isHighEndGfx()) {
+ ThreadedRenderer.disable(true);
+ } else {
+ ThreadedRenderer.enableForegroundTrimming();
+ }
+ ActivityThread thread = new ActivityThread();
+ thread.attach(true);
+ return thread;
+ }
+
+ public final void installSystemProviders(List<ProviderInfo> providers) {
+ if (providers != null) {
+ installContentProviders(mInitialApplication, providers);
+ }
+ }
+
+ public int getIntCoreSetting(String key, int defaultValue) {
+ synchronized (mResourcesManager) {
+ if (mCoreSettings != null) {
+ return mCoreSettings.getInt(key, defaultValue);
+ }
+ return defaultValue;
+ }
+ }
+
+ private static class EventLoggingReporter implements EventLogger.Reporter {
+ @Override
+ public void report (int code, Object... list) {
+ EventLog.writeEvent(code, list);
+ }
+ }
+
+ private class DropBoxReporter implements DropBox.Reporter {
+
+ private DropBoxManager dropBox;
+
+ public DropBoxReporter() {}
+
+ @Override
+ public void addData(String tag, byte[] data, int flags) {
+ ensureInitialized();
+ dropBox.addData(tag, data, flags);
+ }
+
+ @Override
+ public void addText(String tag, String data) {
+ ensureInitialized();
+ dropBox.addText(tag, data);
+ }
+
+ private synchronized void ensureInitialized() {
+ if (dropBox == null) {
+ dropBox = (DropBoxManager) getSystemContext().getSystemService(Context.DROPBOX_SERVICE);
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
+
+ // CloseGuard defaults to true and can be quite spammy. We
+ // disable it here, but selectively enable it later (via
+ // StrictMode) on debug builds, but using DropBox, not logs.
+ CloseGuard.setEnabled(false);
+
+ Environment.initForCurrentUser();
+
+ // Set the reporter for event logging in libcore
+ EventLogger.setReporter(new EventLoggingReporter());
+
+ // Make sure TrustedCertificateStore looks in the right place for CA certificates
+ final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
+ TrustedCertificateStore.setDefaultUserDirectory(configDir);
+
+ Process.setArgV0("<pre-initialized>");
+
+ Looper.prepareMainLooper();
+
+ ActivityThread thread = new ActivityThread();
+ thread.attach(false);
+
+ if (sMainThreadHandler == null) {
+ sMainThreadHandler = thread.getHandler();
+ }
+
+ if (false) {
+ Looper.myLooper().setMessageLogging(new
+ LogPrinter(Log.DEBUG, "ActivityThread"));
+ }
+
+ // End of event ActivityThreadMain.
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ Looper.loop();
+
+ throw new RuntimeException("Main thread loop unexpectedly exited");
+ }
+
+ // ------------------ Regular JNI ------------------------
+
+ private native void nDumpGraphicsInfo(FileDescriptor fd);
+}
diff --git a/android/app/ActivityTransitionCoordinator.java b/android/app/ActivityTransitionCoordinator.java
new file mode 100644
index 00000000..9b2bfc57
--- /dev/null
+++ b/android/app/ActivityTransitionCoordinator.java
@@ -0,0 +1,1116 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.transition.Transition;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionSet;
+import android.transition.Visibility;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.GhostView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.widget.ImageView;
+
+import com.android.internal.view.OneShotPreDrawListener;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Base class for ExitTransitionCoordinator and EnterTransitionCoordinator, classes
+ * that manage activity transitions and the communications coordinating them between
+ * Activities. The ExitTransitionCoordinator is created in the
+ * ActivityOptions#makeSceneTransitionAnimation. The EnterTransitionCoordinator
+ * is created by ActivityOptions#createEnterActivityTransition by Activity when the window is
+ * attached.
+ *
+ * Typical startActivity goes like this:
+ * 1) ExitTransitionCoordinator created with ActivityOptions#makeSceneTransitionAnimation
+ * 2) Activity#startActivity called and that calls startExit() through
+ * ActivityOptions#dispatchStartExit
+ * - Exit transition starts by setting transitioning Views to INVISIBLE
+ * 3) Launched Activity starts, creating an EnterTransitionCoordinator.
+ * - The Window is made translucent
+ * - The Window background alpha is set to 0
+ * - The transitioning views are made INVISIBLE
+ * - MSG_SET_LISTENER is sent back to the ExitTransitionCoordinator.
+ * 4) The shared element transition completes.
+ * - MSG_TAKE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator
+ * 5) The MSG_TAKE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator.
+ * - Shared elements are made VISIBLE
+ * - Shared elements positions and size are set to match the end state of the calling
+ * Activity.
+ * - The shared element transition is started
+ * - If the window allows overlapping transitions, the views transition is started by setting
+ * the entering Views to VISIBLE and the background alpha is animated to opaque.
+ * - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
+ * 6) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
+ * - The shared elements are made INVISIBLE
+ * 7) The exit transition completes in the calling Activity.
+ * - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
+ * 8) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
+ * - If the window doesn't allow overlapping enter transitions, the enter transition is started
+ * by setting entering views to VISIBLE and the background is animated to opaque.
+ * 9) The background opacity animation completes.
+ * - The window is made opaque
+ * 10) The calling Activity gets an onStop() call
+ * - onActivityStopped() is called and all exited Views are made VISIBLE.
+ *
+ * Typical finishAfterTransition goes like this:
+ * 1) finishAfterTransition() creates an ExitTransitionCoordinator and calls startExit()
+ * - The Window start transitioning to Translucent with a new ActivityOptions.
+ * - If no background exists, a black background is substituted
+ * - The shared elements in the scene are matched against those shared elements
+ * that were sent by comparing the names.
+ * - The exit transition is started by setting Views to INVISIBLE.
+ * 2) The ActivityOptions is received by the Activity and an EnterTransitionCoordinator is created.
+ * - All transitioning views are made VISIBLE to reverse what was done when onActivityStopped()
+ * was called
+ * 3) The Window is made translucent and a callback is received
+ * - The background alpha is animated to 0
+ * 4) The background alpha animation completes
+ * 5) The shared element transition completes
+ * - After both 4 & 5 complete, MSG_TAKE_SHARED_ELEMENTS is sent to the
+ * EnterTransitionCoordinator
+ * 6) MSG_TAKE_SHARED_ELEMENTS is received by EnterTransitionCoordinator
+ * - Shared elements are made VISIBLE
+ * - Shared elements positions and size are set to match the end state of the calling
+ * Activity.
+ * - The shared element transition is started
+ * - If the window allows overlapping transitions, the views transition is started by setting
+ * the entering Views to VISIBLE.
+ * - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
+ * 7) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
+ * - The shared elements are made INVISIBLE
+ * 8) The exit transition completes in the finishing Activity.
+ * - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
+ * - finish() is called on the exiting Activity
+ * 9) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
+ * - If the window doesn't allow overlapping enter transitions, the enter transition is started
+ * by setting entering views to VISIBLE.
+ */
+abstract class ActivityTransitionCoordinator extends ResultReceiver {
+ private static final String TAG = "ActivityTransitionCoordinator";
+
+ /**
+ * For Activity transitions, the called Activity's listener to receive calls
+ * when transitions complete.
+ */
+ static final String KEY_REMOTE_RECEIVER = "android:remoteReceiver";
+
+ protected static final String KEY_SCREEN_LEFT = "shared_element:screenLeft";
+ protected static final String KEY_SCREEN_TOP = "shared_element:screenTop";
+ protected static final String KEY_SCREEN_RIGHT = "shared_element:screenRight";
+ protected static final String KEY_SCREEN_BOTTOM= "shared_element:screenBottom";
+ protected static final String KEY_TRANSLATION_Z = "shared_element:translationZ";
+ protected static final String KEY_SNAPSHOT = "shared_element:bitmap";
+ protected static final String KEY_SCALE_TYPE = "shared_element:scaleType";
+ protected static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix";
+ protected static final String KEY_ELEVATION = "shared_element:elevation";
+
+ protected static final ImageView.ScaleType[] SCALE_TYPE_VALUES = ImageView.ScaleType.values();
+
+ /**
+ * Sent by the exiting coordinator (either EnterTransitionCoordinator
+ * or ExitTransitionCoordinator) after the shared elements have
+ * become stationary (shared element transition completes). This tells
+ * the remote coordinator to take control of the shared elements and
+ * that animations may begin. The remote Activity won't start entering
+ * until this message is received, but may wait for
+ * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
+ */
+ public static final int MSG_SET_REMOTE_RECEIVER = 100;
+
+ /**
+ * Sent by the entering coordinator to tell the exiting coordinator
+ * to hide its shared elements after it has started its shared
+ * element transition. This is temporary until the
+ * interlock of shared elements is figured out.
+ */
+ public static final int MSG_HIDE_SHARED_ELEMENTS = 101;
+
+ /**
+ * Sent by the exiting coordinator (either EnterTransitionCoordinator
+ * or ExitTransitionCoordinator) after the shared elements have
+ * become stationary (shared element transition completes). This tells
+ * the remote coordinator to take control of the shared elements and
+ * that animations may begin. The remote Activity won't start entering
+ * until this message is received, but may wait for
+ * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
+ */
+ public static final int MSG_TAKE_SHARED_ELEMENTS = 103;
+
+ /**
+ * Sent by the exiting coordinator (either
+ * EnterTransitionCoordinator or ExitTransitionCoordinator) after
+ * the exiting Views have finished leaving the scene. This will
+ * be ignored if allowOverlappingTransitions() is true on the
+ * remote coordinator. If it is false, it will trigger the enter
+ * transition to start.
+ */
+ public static final int MSG_EXIT_TRANSITION_COMPLETE = 104;
+
+ /**
+ * Sent by Activity#startActivity to begin the exit transition.
+ */
+ public static final int MSG_START_EXIT_TRANSITION = 105;
+
+ /**
+ * It took too long for a message from the entering Activity, so we canceled the transition.
+ */
+ public static final int MSG_CANCEL = 106;
+
+ /**
+ * When returning, this is the destination location for the shared element.
+ */
+ public static final int MSG_SHARED_ELEMENT_DESTINATION = 107;
+
+ private Window mWindow;
+ final protected ArrayList<String> mAllSharedElementNames;
+ final protected ArrayList<View> mSharedElements = new ArrayList<View>();
+ final protected ArrayList<String> mSharedElementNames = new ArrayList<String>();
+ protected ArrayList<View> mTransitioningViews = new ArrayList<View>();
+ protected SharedElementCallback mListener;
+ protected ResultReceiver mResultReceiver;
+ final private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback();
+ final protected boolean mIsReturning;
+ private Runnable mPendingTransition;
+ private boolean mIsStartingTransition;
+ private ArrayList<GhostViewListeners> mGhostViewListeners =
+ new ArrayList<GhostViewListeners>();
+ private ArrayMap<View, Float> mOriginalAlphas = new ArrayMap<View, Float>();
+ private ArrayList<Matrix> mSharedElementParentMatrices;
+ private boolean mSharedElementTransitionComplete;
+ private boolean mViewsTransitionComplete;
+ private boolean mBackgroundAnimatorComplete;
+ private ArrayList<View> mStrippedTransitioningViews = new ArrayList<>();
+
+ public ActivityTransitionCoordinator(Window window,
+ ArrayList<String> allSharedElementNames,
+ SharedElementCallback listener, boolean isReturning) {
+ super(new Handler());
+ mWindow = window;
+ mListener = listener;
+ mAllSharedElementNames = allSharedElementNames;
+ mIsReturning = isReturning;
+ }
+
+ protected void viewsReady(ArrayMap<String, View> sharedElements) {
+ sharedElements.retainAll(mAllSharedElementNames);
+ if (mListener != null) {
+ mListener.onMapSharedElements(mAllSharedElementNames, sharedElements);
+ }
+ setSharedElements(sharedElements);
+ if (getViewsTransition() != null && mTransitioningViews != null) {
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.captureTransitioningViews(mTransitioningViews);
+ }
+ mTransitioningViews.removeAll(mSharedElements);
+ }
+ setEpicenter();
+ }
+
+ /**
+ * Iterates over the shared elements and adds them to the members in order.
+ * Shared elements that are nested in other shared elements are placed after the
+ * elements that they are nested in. This means that layout ordering can be done
+ * from first to last.
+ *
+ * @param sharedElements The map of transition names to shared elements to set into
+ * the member fields.
+ */
+ private void setSharedElements(ArrayMap<String, View> sharedElements) {
+ boolean isFirstRun = true;
+ while (!sharedElements.isEmpty()) {
+ final int numSharedElements = sharedElements.size();
+ for (int i = numSharedElements - 1; i >= 0; i--) {
+ final View view = sharedElements.valueAt(i);
+ final String name = sharedElements.keyAt(i);
+ if (isFirstRun && (view == null || !view.isAttachedToWindow() || name == null)) {
+ sharedElements.removeAt(i);
+ } else if (!isNested(view, sharedElements)) {
+ mSharedElementNames.add(name);
+ mSharedElements.add(view);
+ sharedElements.removeAt(i);
+ }
+ }
+ isFirstRun = false;
+ }
+ }
+
+ /**
+ * Returns true when view is nested in any of the values of sharedElements.
+ */
+ private static boolean isNested(View view, ArrayMap<String, View> sharedElements) {
+ ViewParent parent = view.getParent();
+ boolean isNested = false;
+ while (parent instanceof View) {
+ View parentView = (View) parent;
+ if (sharedElements.containsValue(parentView)) {
+ isNested = true;
+ break;
+ }
+ parent = parentView.getParent();
+ }
+ return isNested;
+ }
+
+ protected void stripOffscreenViews() {
+ if (mTransitioningViews == null) {
+ return;
+ }
+ Rect r = new Rect();
+ for (int i = mTransitioningViews.size() - 1; i >= 0; i--) {
+ View view = mTransitioningViews.get(i);
+ if (!view.getGlobalVisibleRect(r)) {
+ mTransitioningViews.remove(i);
+ mStrippedTransitioningViews.add(view);
+ }
+ }
+ }
+
+ protected Window getWindow() {
+ return mWindow;
+ }
+
+ public ViewGroup getDecor() {
+ return (mWindow == null) ? null : (ViewGroup) mWindow.getDecorView();
+ }
+
+ /**
+ * Sets the transition epicenter to the position of the first shared element.
+ */
+ protected void setEpicenter() {
+ View epicenter = null;
+ if (!mAllSharedElementNames.isEmpty() && !mSharedElementNames.isEmpty()) {
+ int index = mSharedElementNames.indexOf(mAllSharedElementNames.get(0));
+ if (index >= 0) {
+ epicenter = mSharedElements.get(index);
+ }
+ }
+ setEpicenter(epicenter);
+ }
+
+ private void setEpicenter(View view) {
+ if (view == null) {
+ mEpicenterCallback.setEpicenter(null);
+ } else {
+ Rect epicenter = new Rect();
+ view.getBoundsOnScreen(epicenter);
+ mEpicenterCallback.setEpicenter(epicenter);
+ }
+ }
+
+ public ArrayList<String> getAcceptedNames() {
+ return mSharedElementNames;
+ }
+
+ public ArrayList<String> getMappedNames() {
+ ArrayList<String> names = new ArrayList<String>(mSharedElements.size());
+ for (int i = 0; i < mSharedElements.size(); i++) {
+ names.add(mSharedElements.get(i).getTransitionName());
+ }
+ return names;
+ }
+
+ public ArrayList<View> copyMappedViews() {
+ return new ArrayList<View>(mSharedElements);
+ }
+
+ public ArrayList<String> getAllSharedElementNames() { return mAllSharedElementNames; }
+
+ protected Transition setTargets(Transition transition, boolean add) {
+ if (transition == null || (add &&
+ (mTransitioningViews == null || mTransitioningViews.isEmpty()))) {
+ return null;
+ }
+ // Add the targets to a set containing transition so that transition
+ // remains unaffected. We don't want to modify the targets of transition itself.
+ TransitionSet set = new TransitionSet();
+ if (mTransitioningViews != null) {
+ for (int i = mTransitioningViews.size() - 1; i >= 0; i--) {
+ View view = mTransitioningViews.get(i);
+ if (add) {
+ set.addTarget(view);
+ } else {
+ set.excludeTarget(view, true);
+ }
+ }
+ }
+ if (mStrippedTransitioningViews != null) {
+ for (int i = mStrippedTransitioningViews.size() - 1; i >= 0; i--) {
+ View view = mStrippedTransitioningViews.get(i);
+ set.excludeTarget(view, true);
+ }
+ }
+ // By adding the transition after addTarget, we prevent addTarget from
+ // affecting transition.
+ set.addTransition(transition);
+
+ if (!add && mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
+ // Allow children of excluded transitioning views, but not the views themselves
+ set = new TransitionSet().addTransition(set);
+ }
+
+ return set;
+ }
+
+ protected Transition configureTransition(Transition transition,
+ boolean includeTransitioningViews) {
+ if (transition != null) {
+ transition = transition.clone();
+ transition.setEpicenterCallback(mEpicenterCallback);
+ transition = setTargets(transition, includeTransitioningViews);
+ }
+ noLayoutSuppressionForVisibilityTransitions(transition);
+ return transition;
+ }
+
+ /**
+ * Looks through the transition to see which Views have been included and which have been
+ * excluded. {@code views} will be modified to contain only those Views that are included
+ * in the transition. If {@code transition} is a TransitionSet, it will search through all
+ * contained Transitions to find targeted Views.
+ *
+ * @param transition The transition to look through for inclusion of Views
+ * @param views The list of Views that are to be checked for inclusion. Will be modified
+ * to remove all excluded Views, possibly leaving an empty list.
+ */
+ protected static void removeExcludedViews(Transition transition, ArrayList<View> views) {
+ ArraySet<View> included = new ArraySet<>();
+ findIncludedViews(transition, views, included);
+ views.clear();
+ views.addAll(included);
+ }
+
+ /**
+ * Looks through the transition to see which Views have been included. Only {@code views}
+ * will be examined for inclusion. If {@code transition} is a TransitionSet, it will search
+ * through all contained Transitions to find targeted Views.
+ *
+ * @param transition The transition to look through for inclusion of Views
+ * @param views The list of Views that are to be checked for inclusion.
+ * @param included Modified to contain all Views in views that have at least one Transition
+ * that affects it.
+ */
+ private static void findIncludedViews(Transition transition, ArrayList<View> views,
+ ArraySet<View> included) {
+ if (transition instanceof TransitionSet) {
+ TransitionSet set = (TransitionSet) transition;
+ ArrayList<View> includedViews = new ArrayList<>();
+ final int numViews = views.size();
+ for (int i = 0; i < numViews; i++) {
+ final View view = views.get(i);
+ if (transition.isValidTarget(view)) {
+ includedViews.add(view);
+ }
+ }
+ final int count = set.getTransitionCount();
+ for (int i = 0; i < count; i++) {
+ findIncludedViews(set.getTransitionAt(i), includedViews, included);
+ }
+ } else {
+ final int numViews = views.size();
+ for (int i = 0; i < numViews; i++) {
+ final View view = views.get(i);
+ if (transition.isValidTarget(view)) {
+ included.add(view);
+ }
+ }
+ }
+ }
+
+ protected static Transition mergeTransitions(Transition transition1, Transition transition2) {
+ if (transition1 == null) {
+ return transition2;
+ } else if (transition2 == null) {
+ return transition1;
+ } else {
+ TransitionSet transitionSet = new TransitionSet();
+ transitionSet.addTransition(transition1);
+ transitionSet.addTransition(transition2);
+ return transitionSet;
+ }
+ }
+
+ protected ArrayMap<String, View> mapSharedElements(ArrayList<String> accepted,
+ ArrayList<View> localViews) {
+ ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
+ if (accepted != null) {
+ for (int i = 0; i < accepted.size(); i++) {
+ sharedElements.put(accepted.get(i), localViews.get(i));
+ }
+ } else {
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.findNamedViews(sharedElements);
+ }
+ }
+ return sharedElements;
+ }
+
+ protected void setResultReceiver(ResultReceiver resultReceiver) {
+ mResultReceiver = resultReceiver;
+ }
+
+ protected abstract Transition getViewsTransition();
+
+ private void setSharedElementState(View view, String name, Bundle transitionArgs,
+ Matrix tempMatrix, RectF tempRect, int[] decorLoc) {
+ Bundle sharedElementBundle = transitionArgs.getBundle(name);
+ if (sharedElementBundle == null) {
+ return;
+ }
+
+ if (view instanceof ImageView) {
+ int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1);
+ if (scaleTypeInt >= 0) {
+ ImageView imageView = (ImageView) view;
+ ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt];
+ imageView.setScaleType(scaleType);
+ if (scaleType == ImageView.ScaleType.MATRIX) {
+ float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX);
+ tempMatrix.setValues(matrixValues);
+ imageView.setImageMatrix(tempMatrix);
+ }
+ }
+ }
+
+ float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z);
+ view.setTranslationZ(z);
+ float elevation = sharedElementBundle.getFloat(KEY_ELEVATION);
+ view.setElevation(elevation);
+
+ float left = sharedElementBundle.getFloat(KEY_SCREEN_LEFT);
+ float top = sharedElementBundle.getFloat(KEY_SCREEN_TOP);
+ float right = sharedElementBundle.getFloat(KEY_SCREEN_RIGHT);
+ float bottom = sharedElementBundle.getFloat(KEY_SCREEN_BOTTOM);
+
+ if (decorLoc != null) {
+ left -= decorLoc[0];
+ top -= decorLoc[1];
+ right -= decorLoc[0];
+ bottom -= decorLoc[1];
+ } else {
+ // Find the location in the view's parent
+ getSharedElementParentMatrix(view, tempMatrix);
+ tempRect.set(left, top, right, bottom);
+ tempMatrix.mapRect(tempRect);
+
+ float leftInParent = tempRect.left;
+ float topInParent = tempRect.top;
+
+ // Find the size of the view
+ view.getInverseMatrix().mapRect(tempRect);
+ float width = tempRect.width();
+ float height = tempRect.height();
+
+ // Now determine the offset due to view transform:
+ view.setLeft(0);
+ view.setTop(0);
+ view.setRight(Math.round(width));
+ view.setBottom(Math.round(height));
+ tempRect.set(0, 0, width, height);
+ view.getMatrix().mapRect(tempRect);
+
+ left = leftInParent - tempRect.left;
+ top = topInParent - tempRect.top;
+ right = left + width;
+ bottom = top + height;
+ }
+
+ int x = Math.round(left);
+ int y = Math.round(top);
+ int width = Math.round(right) - x;
+ int height = Math.round(bottom) - y;
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ view.measure(widthSpec, heightSpec);
+
+ view.layout(x, y, x + width, y + height);
+ }
+
+ private void setSharedElementMatrices() {
+ int numSharedElements = mSharedElements.size();
+ if (numSharedElements > 0) {
+ mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements);
+ }
+ for (int i = 0; i < numSharedElements; i++) {
+ View view = mSharedElements.get(i);
+
+ // Find the location in the view's parent
+ ViewGroup parent = (ViewGroup) view.getParent();
+ Matrix matrix = new Matrix();
+ if (parent != null) {
+ parent.transformMatrixToLocal(matrix);
+ matrix.postTranslate(parent.getScrollX(), parent.getScrollY());
+ }
+ mSharedElementParentMatrices.add(matrix);
+ }
+ }
+
+ private void getSharedElementParentMatrix(View view, Matrix matrix) {
+ final int index = mSharedElementParentMatrices == null ? -1
+ : mSharedElements.indexOf(view);
+ if (index < 0) {
+ matrix.reset();
+ ViewParent viewParent = view.getParent();
+ if (viewParent instanceof ViewGroup) {
+ // Find the location in the view's parent
+ ViewGroup parent = (ViewGroup) viewParent;
+ parent.transformMatrixToLocal(matrix);
+ matrix.postTranslate(parent.getScrollX(), parent.getScrollY());
+ }
+ } else {
+ // The indices of mSharedElementParentMatrices matches the
+ // mSharedElement matrices.
+ Matrix parentMatrix = mSharedElementParentMatrices.get(index);
+ matrix.set(parentMatrix);
+ }
+ }
+
+ protected ArrayList<SharedElementOriginalState> setSharedElementState(
+ Bundle sharedElementState, final ArrayList<View> snapshots) {
+ ArrayList<SharedElementOriginalState> originalImageState =
+ new ArrayList<SharedElementOriginalState>();
+ if (sharedElementState != null) {
+ Matrix tempMatrix = new Matrix();
+ RectF tempRect = new RectF();
+ final int numSharedElements = mSharedElements.size();
+ for (int i = 0; i < numSharedElements; i++) {
+ View sharedElement = mSharedElements.get(i);
+ String name = mSharedElementNames.get(i);
+ SharedElementOriginalState originalState = getOldSharedElementState(sharedElement,
+ name, sharedElementState);
+ originalImageState.add(originalState);
+ setSharedElementState(sharedElement, name, sharedElementState,
+ tempMatrix, tempRect, null);
+ }
+ }
+ if (mListener != null) {
+ mListener.onSharedElementStart(mSharedElementNames, mSharedElements, snapshots);
+ }
+ return originalImageState;
+ }
+
+ protected void notifySharedElementEnd(ArrayList<View> snapshots) {
+ if (mListener != null) {
+ mListener.onSharedElementEnd(mSharedElementNames, mSharedElements, snapshots);
+ }
+ }
+
+ protected void scheduleSetSharedElementEnd(final ArrayList<View> snapshots) {
+ final View decorView = getDecor();
+ if (decorView != null) {
+ OneShotPreDrawListener.add(decorView, () -> {
+ notifySharedElementEnd(snapshots);
+ });
+ }
+ }
+
+ private static SharedElementOriginalState getOldSharedElementState(View view, String name,
+ Bundle transitionArgs) {
+
+ SharedElementOriginalState state = new SharedElementOriginalState();
+ state.mLeft = view.getLeft();
+ state.mTop = view.getTop();
+ state.mRight = view.getRight();
+ state.mBottom = view.getBottom();
+ state.mMeasuredWidth = view.getMeasuredWidth();
+ state.mMeasuredHeight = view.getMeasuredHeight();
+ state.mTranslationZ = view.getTranslationZ();
+ state.mElevation = view.getElevation();
+ if (!(view instanceof ImageView)) {
+ return state;
+ }
+ Bundle bundle = transitionArgs.getBundle(name);
+ if (bundle == null) {
+ return state;
+ }
+ int scaleTypeInt = bundle.getInt(KEY_SCALE_TYPE, -1);
+ if (scaleTypeInt < 0) {
+ return state;
+ }
+
+ ImageView imageView = (ImageView) view;
+ state.mScaleType = imageView.getScaleType();
+ if (state.mScaleType == ImageView.ScaleType.MATRIX) {
+ state.mMatrix = new Matrix(imageView.getImageMatrix());
+ }
+ return state;
+ }
+
+ protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) {
+ int numSharedElements = names.size();
+ ArrayList<View> snapshots = new ArrayList<View>(numSharedElements);
+ if (numSharedElements == 0) {
+ return snapshots;
+ }
+ Context context = getWindow().getContext();
+ int[] decorLoc = new int[2];
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.getLocationOnScreen(decorLoc);
+ }
+ Matrix tempMatrix = new Matrix();
+ for (String name: names) {
+ Bundle sharedElementBundle = state.getBundle(name);
+ View snapshot = null;
+ if (sharedElementBundle != null) {
+ Parcelable parcelable = sharedElementBundle.getParcelable(KEY_SNAPSHOT);
+ if (parcelable != null && mListener != null) {
+ snapshot = mListener.onCreateSnapshotView(context, parcelable);
+ }
+ if (snapshot != null) {
+ setSharedElementState(snapshot, name, state, tempMatrix, null, decorLoc);
+ }
+ }
+ // Even null snapshots are added so they remain in the same order as shared elements.
+ snapshots.add(snapshot);
+ }
+ return snapshots;
+ }
+
+ protected static void setOriginalSharedElementState(ArrayList<View> sharedElements,
+ ArrayList<SharedElementOriginalState> originalState) {
+ for (int i = 0; i < originalState.size(); i++) {
+ View view = sharedElements.get(i);
+ SharedElementOriginalState state = originalState.get(i);
+ if (view instanceof ImageView && state.mScaleType != null) {
+ ImageView imageView = (ImageView) view;
+ imageView.setScaleType(state.mScaleType);
+ if (state.mScaleType == ImageView.ScaleType.MATRIX) {
+ imageView.setImageMatrix(state.mMatrix);
+ }
+ }
+ view.setElevation(state.mElevation);
+ view.setTranslationZ(state.mTranslationZ);
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredWidth,
+ View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredHeight,
+ View.MeasureSpec.EXACTLY);
+ view.measure(widthSpec, heightSpec);
+ view.layout(state.mLeft, state.mTop, state.mRight, state.mBottom);
+ }
+ }
+
+ protected Bundle captureSharedElementState() {
+ Bundle bundle = new Bundle();
+ RectF tempBounds = new RectF();
+ Matrix tempMatrix = new Matrix();
+ for (int i = 0; i < mSharedElements.size(); i++) {
+ View sharedElement = mSharedElements.get(i);
+ String name = mSharedElementNames.get(i);
+ captureSharedElementState(sharedElement, name, bundle, tempMatrix, tempBounds);
+ }
+ return bundle;
+ }
+
+ protected void clearState() {
+ // Clear the state so that we can't hold any references accidentally and leak memory.
+ mWindow = null;
+ mSharedElements.clear();
+ mTransitioningViews = null;
+ mStrippedTransitioningViews = null;
+ mOriginalAlphas.clear();
+ mResultReceiver = null;
+ mPendingTransition = null;
+ mListener = null;
+ mSharedElementParentMatrices = null;
+ }
+
+ protected long getFadeDuration() {
+ return getWindow().getTransitionBackgroundFadeDuration();
+ }
+
+ protected void hideViews(ArrayList<View> views) {
+ int count = views.size();
+ for (int i = 0; i < count; i++) {
+ View view = views.get(i);
+ if (!mOriginalAlphas.containsKey(view)) {
+ mOriginalAlphas.put(view, view.getAlpha());
+ }
+ view.setAlpha(0f);
+ }
+ }
+
+ protected void showViews(ArrayList<View> views, boolean setTransitionAlpha) {
+ int count = views.size();
+ for (int i = 0; i < count; i++) {
+ showView(views.get(i), setTransitionAlpha);
+ }
+ }
+
+ private void showView(View view, boolean setTransitionAlpha) {
+ Float alpha = mOriginalAlphas.remove(view);
+ if (alpha != null) {
+ view.setAlpha(alpha);
+ }
+ if (setTransitionAlpha) {
+ view.setTransitionAlpha(1f);
+ }
+ }
+
+ /**
+ * Captures placement information for Views with a shared element name for
+ * Activity Transitions.
+ *
+ * @param view The View to capture the placement information for.
+ * @param name The shared element name in the target Activity to apply the placement
+ * information for.
+ * @param transitionArgs Bundle to store shared element placement information.
+ * @param tempBounds A temporary Rect for capturing the current location of views.
+ */
+ protected void captureSharedElementState(View view, String name, Bundle transitionArgs,
+ Matrix tempMatrix, RectF tempBounds) {
+ Bundle sharedElementBundle = new Bundle();
+ tempMatrix.reset();
+ view.transformMatrixToGlobal(tempMatrix);
+ tempBounds.set(0, 0, view.getWidth(), view.getHeight());
+ tempMatrix.mapRect(tempBounds);
+
+ sharedElementBundle.putFloat(KEY_SCREEN_LEFT, tempBounds.left);
+ sharedElementBundle.putFloat(KEY_SCREEN_RIGHT, tempBounds.right);
+ sharedElementBundle.putFloat(KEY_SCREEN_TOP, tempBounds.top);
+ sharedElementBundle.putFloat(KEY_SCREEN_BOTTOM, tempBounds.bottom);
+ sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ());
+ sharedElementBundle.putFloat(KEY_ELEVATION, view.getElevation());
+
+ Parcelable bitmap = null;
+ if (mListener != null) {
+ bitmap = mListener.onCaptureSharedElementSnapshot(view, tempMatrix, tempBounds);
+ }
+
+ if (bitmap != null) {
+ sharedElementBundle.putParcelable(KEY_SNAPSHOT, bitmap);
+ }
+
+ if (view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ int scaleTypeInt = scaleTypeToInt(imageView.getScaleType());
+ sharedElementBundle.putInt(KEY_SCALE_TYPE, scaleTypeInt);
+ if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) {
+ float[] matrix = new float[9];
+ imageView.getImageMatrix().getValues(matrix);
+ sharedElementBundle.putFloatArray(KEY_IMAGE_MATRIX, matrix);
+ }
+ }
+
+ transitionArgs.putBundle(name, sharedElementBundle);
+ }
+
+
+ protected void startTransition(Runnable runnable) {
+ if (mIsStartingTransition) {
+ mPendingTransition = runnable;
+ } else {
+ mIsStartingTransition = true;
+ runnable.run();
+ }
+ }
+
+ protected void transitionStarted() {
+ mIsStartingTransition = false;
+ }
+
+ /**
+ * Cancels any pending transitions and returns true if there is a transition is in
+ * the middle of starting.
+ */
+ protected boolean cancelPendingTransitions() {
+ mPendingTransition = null;
+ return mIsStartingTransition;
+ }
+
+ protected void moveSharedElementsToOverlay() {
+ if (mWindow == null || !mWindow.getSharedElementsUseOverlay()) {
+ return;
+ }
+ setSharedElementMatrices();
+ int numSharedElements = mSharedElements.size();
+ ViewGroup decor = getDecor();
+ if (decor != null) {
+ boolean moveWithParent = moveSharedElementWithParent();
+ Matrix tempMatrix = new Matrix();
+ for (int i = 0; i < numSharedElements; i++) {
+ View view = mSharedElements.get(i);
+ if (view.isAttachedToWindow()) {
+ tempMatrix.reset();
+ mSharedElementParentMatrices.get(i).invert(tempMatrix);
+ GhostView.addGhost(view, decor, tempMatrix);
+ ViewGroup parent = (ViewGroup) view.getParent();
+ if (moveWithParent && !isInTransitionGroup(parent, decor)) {
+ GhostViewListeners listener = new GhostViewListeners(view, parent, decor);
+ parent.getViewTreeObserver().addOnPreDrawListener(listener);
+ parent.addOnAttachStateChangeListener(listener);
+ mGhostViewListeners.add(listener);
+ }
+ }
+ }
+ }
+ }
+
+ protected boolean moveSharedElementWithParent() {
+ return true;
+ }
+
+ public static boolean isInTransitionGroup(ViewParent viewParent, ViewGroup decor) {
+ if (viewParent == decor || !(viewParent instanceof ViewGroup)) {
+ return false;
+ }
+ ViewGroup parent = (ViewGroup) viewParent;
+ if (parent.isTransitionGroup()) {
+ return true;
+ } else {
+ return isInTransitionGroup(parent.getParent(), decor);
+ }
+ }
+
+ protected void moveSharedElementsFromOverlay() {
+ int numListeners = mGhostViewListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ GhostViewListeners listener = mGhostViewListeners.get(i);
+ listener.removeListener();
+ }
+ mGhostViewListeners.clear();
+
+ if (mWindow == null || !mWindow.getSharedElementsUseOverlay()) {
+ return;
+ }
+ ViewGroup decor = getDecor();
+ if (decor != null) {
+ ViewGroupOverlay overlay = decor.getOverlay();
+ int count = mSharedElements.size();
+ for (int i = 0; i < count; i++) {
+ View sharedElement = mSharedElements.get(i);
+ GhostView.removeGhost(sharedElement);
+ }
+ }
+ }
+
+ protected void setGhostVisibility(int visibility) {
+ int numSharedElements = mSharedElements.size();
+ for (int i = 0; i < numSharedElements; i++) {
+ GhostView ghostView = GhostView.getGhost(mSharedElements.get(i));
+ if (ghostView != null) {
+ ghostView.setVisibility(visibility);
+ }
+ }
+ }
+
+ protected void scheduleGhostVisibilityChange(final int visibility) {
+ final View decorView = getDecor();
+ if (decorView != null) {
+ OneShotPreDrawListener.add(decorView, () -> {
+ setGhostVisibility(visibility);
+ });
+ }
+ }
+
+ protected boolean isViewsTransitionComplete() {
+ return mViewsTransitionComplete;
+ }
+
+ protected void viewsTransitionComplete() {
+ mViewsTransitionComplete = true;
+ startInputWhenTransitionsComplete();
+ }
+
+ protected void backgroundAnimatorComplete() {
+ mBackgroundAnimatorComplete = true;
+ }
+
+ protected void sharedElementTransitionComplete() {
+ mSharedElementTransitionComplete = true;
+ startInputWhenTransitionsComplete();
+ }
+ private void startInputWhenTransitionsComplete() {
+ if (mViewsTransitionComplete && mSharedElementTransitionComplete) {
+ final View decor = getDecor();
+ if (decor != null) {
+ final ViewRootImpl viewRoot = decor.getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.setPausedForTransition(false);
+ }
+ }
+ onTransitionsComplete();
+ }
+ }
+
+ protected void pauseInput() {
+ final View decor = getDecor();
+ final ViewRootImpl viewRoot = decor == null ? null : decor.getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.setPausedForTransition(true);
+ }
+ }
+
+ protected void onTransitionsComplete() {}
+
+ protected class ContinueTransitionListener extends TransitionListenerAdapter {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ mIsStartingTransition = false;
+ Runnable pending = mPendingTransition;
+ mPendingTransition = null;
+ if (pending != null) {
+ startTransition(pending);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ }
+ }
+
+ private static int scaleTypeToInt(ImageView.ScaleType scaleType) {
+ for (int i = 0; i < SCALE_TYPE_VALUES.length; i++) {
+ if (scaleType == SCALE_TYPE_VALUES[i]) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected void setTransitioningViewsVisiblity(int visiblity, boolean invalidate) {
+ final int numElements = mTransitioningViews == null ? 0 : mTransitioningViews.size();
+ for (int i = 0; i < numElements; i++) {
+ final View view = mTransitioningViews.get(i);
+ if (invalidate) {
+ // Allow the view to be invalidated by the visibility change
+ view.setVisibility(visiblity);
+ } else {
+ // Don't invalidate the view with the visibility change
+ view.setTransitionVisibility(visiblity);
+ }
+ }
+ }
+
+ /**
+ * Blocks suppressLayout from Visibility transitions. It is ok to suppress the layout,
+ * but we don't want to force the layout when suppressLayout becomes false. This leads
+ * to visual glitches.
+ */
+ private static void noLayoutSuppressionForVisibilityTransitions(Transition transition) {
+ if (transition instanceof Visibility) {
+ final Visibility visibility = (Visibility) transition;
+ visibility.setSuppressLayout(false);
+ } else if (transition instanceof TransitionSet) {
+ final TransitionSet set = (TransitionSet) transition;
+ final int count = set.getTransitionCount();
+ for (int i = 0; i < count; i++) {
+ noLayoutSuppressionForVisibilityTransitions(set.getTransitionAt(i));
+ }
+ }
+ }
+
+ public boolean isTransitionRunning() {
+ return !(mViewsTransitionComplete && mSharedElementTransitionComplete &&
+ mBackgroundAnimatorComplete);
+ }
+
+ private static class FixedEpicenterCallback extends Transition.EpicenterCallback {
+ private Rect mEpicenter;
+
+ public void setEpicenter(Rect epicenter) { mEpicenter = epicenter; }
+
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return mEpicenter;
+ }
+ }
+
+ private static class GhostViewListeners implements ViewTreeObserver.OnPreDrawListener,
+ View.OnAttachStateChangeListener {
+ private View mView;
+ private ViewGroup mDecor;
+ private View mParent;
+ private Matrix mMatrix = new Matrix();
+ private ViewTreeObserver mViewTreeObserver;
+
+ public GhostViewListeners(View view, View parent, ViewGroup decor) {
+ mView = view;
+ mParent = parent;
+ mDecor = decor;
+ mViewTreeObserver = parent.getViewTreeObserver();
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ GhostView ghostView = GhostView.getGhost(mView);
+ if (ghostView == null || !mView.isAttachedToWindow()) {
+ removeListener();
+ } else {
+ GhostView.calculateMatrix(mView, mDecor, mMatrix);
+ ghostView.setMatrix(mMatrix);
+ }
+ return true;
+ }
+
+ public void removeListener() {
+ if (mViewTreeObserver.isAlive()) {
+ mViewTreeObserver.removeOnPreDrawListener(this);
+ } else {
+ mParent.getViewTreeObserver().removeOnPreDrawListener(this);
+ }
+ mParent.removeOnAttachStateChangeListener(this);
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mViewTreeObserver = v.getViewTreeObserver();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removeListener();
+ }
+ }
+
+ static class SharedElementOriginalState {
+ int mLeft;
+ int mTop;
+ int mRight;
+ int mBottom;
+ int mMeasuredWidth;
+ int mMeasuredHeight;
+ ImageView.ScaleType mScaleType;
+ Matrix mMatrix;
+ float mTranslationZ;
+ float mElevation;
+ }
+}
diff --git a/android/app/ActivityTransitionState.java b/android/app/ActivityTransitionState.java
new file mode 100644
index 00000000..b8f5a8e9
--- /dev/null
+++ b/android/app/ActivityTransitionState.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.transition.Transition;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import com.android.internal.view.OneShotPreDrawListener;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * This class contains all persistence-related functionality for Activity Transitions.
+ * Activities start exit and enter Activity Transitions through this class.
+ */
+class ActivityTransitionState {
+
+ private static final String ENTERING_SHARED_ELEMENTS = "android:enteringSharedElements";
+
+ private static final String EXITING_MAPPED_FROM = "android:exitingMappedFrom";
+
+ private static final String EXITING_MAPPED_TO = "android:exitingMappedTo";
+
+ /**
+ * The shared elements that the calling Activity has said that they transferred to this
+ * Activity.
+ */
+ private ArrayList<String> mEnteringNames;
+
+ /**
+ * The names of shared elements that were shared to the called Activity.
+ */
+ private ArrayList<String> mExitingFrom;
+
+ /**
+ * The names of local Views that were shared out, mapped to those elements in mExitingFrom.
+ */
+ private ArrayList<String> mExitingTo;
+
+ /**
+ * The local Views that were shared out, mapped to those elements in mExitingFrom.
+ */
+ private ArrayList<View> mExitingToView;
+
+ /**
+ * The ExitTransitionCoordinator used to start an Activity. Used to make the elements restore
+ * Visibility of exited Views.
+ */
+ private ExitTransitionCoordinator mCalledExitCoordinator;
+
+ /**
+ * The ExitTransitionCoordinator used to return to a previous Activity when called with
+ * {@link android.app.Activity#finishAfterTransition()}.
+ */
+ private ExitTransitionCoordinator mReturnExitCoordinator;
+
+ /**
+ * We must be able to cancel entering transitions to stop changing the Window to
+ * opaque when we exit before making the Window opaque.
+ */
+ private EnterTransitionCoordinator mEnterTransitionCoordinator;
+
+ /**
+ * ActivityOptions used on entering this Activity.
+ */
+ private ActivityOptions mEnterActivityOptions;
+
+ /**
+ * Has an exit transition been started? If so, we don't want to double-exit.
+ */
+ private boolean mHasExited;
+
+ /**
+ * Postpone painting and starting the enter transition until this is false.
+ */
+ private boolean mIsEnterPostponed;
+
+ /**
+ * Potential exit transition coordinators.
+ */
+ private SparseArray<WeakReference<ExitTransitionCoordinator>> mExitTransitionCoordinators;
+
+ /**
+ * Next key for mExitTransitionCoordinator.
+ */
+ private int mExitTransitionCoordinatorsKey = 1;
+
+ private boolean mIsEnterTriggered;
+
+ public ActivityTransitionState() {
+ }
+
+ public int addExitTransitionCoordinator(ExitTransitionCoordinator exitTransitionCoordinator) {
+ if (mExitTransitionCoordinators == null) {
+ mExitTransitionCoordinators =
+ new SparseArray<WeakReference<ExitTransitionCoordinator>>();
+ }
+ WeakReference<ExitTransitionCoordinator> ref = new WeakReference(exitTransitionCoordinator);
+ // clean up old references:
+ for (int i = mExitTransitionCoordinators.size() - 1; i >= 0; i--) {
+ WeakReference<ExitTransitionCoordinator> oldRef
+ = mExitTransitionCoordinators.valueAt(i);
+ if (oldRef.get() == null) {
+ mExitTransitionCoordinators.removeAt(i);
+ }
+ }
+ int newKey = mExitTransitionCoordinatorsKey++;
+ mExitTransitionCoordinators.append(newKey, ref);
+ return newKey;
+ }
+
+ public void readState(Bundle bundle) {
+ if (bundle != null) {
+ if (mEnterTransitionCoordinator == null || mEnterTransitionCoordinator.isReturning()) {
+ mEnteringNames = bundle.getStringArrayList(ENTERING_SHARED_ELEMENTS);
+ }
+ if (mEnterTransitionCoordinator == null) {
+ mExitingFrom = bundle.getStringArrayList(EXITING_MAPPED_FROM);
+ mExitingTo = bundle.getStringArrayList(EXITING_MAPPED_TO);
+ }
+ }
+ }
+
+ public void saveState(Bundle bundle) {
+ if (mEnteringNames != null) {
+ bundle.putStringArrayList(ENTERING_SHARED_ELEMENTS, mEnteringNames);
+ }
+ if (mExitingFrom != null) {
+ bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom);
+ bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo);
+ }
+ }
+
+ public void setEnterActivityOptions(Activity activity, ActivityOptions options) {
+ final Window window = activity.getWindow();
+ if (window == null) {
+ return;
+ }
+ // ensure Decor View has been created so that the window features are activated
+ window.getDecorView();
+ if (window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
+ && options != null && mEnterActivityOptions == null
+ && mEnterTransitionCoordinator == null
+ && options.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
+ mEnterActivityOptions = options;
+ mIsEnterTriggered = false;
+ if (mEnterActivityOptions.isReturning()) {
+ restoreExitedViews();
+ int result = mEnterActivityOptions.getResultCode();
+ if (result != 0) {
+ Intent intent = mEnterActivityOptions.getResultData();
+ if (intent != null) {
+ intent.setExtrasClassLoader(activity.getClassLoader());
+ }
+ activity.onActivityReenter(result, intent);
+ }
+ }
+ }
+ }
+
+ public void enterReady(Activity activity) {
+ if (mEnterActivityOptions == null || mIsEnterTriggered) {
+ return;
+ }
+ mIsEnterTriggered = true;
+ mHasExited = false;
+ ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
+ ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
+ if (mEnterActivityOptions.isReturning()) {
+ restoreExitedViews();
+ activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
+ }
+ mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
+ resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
+ mEnterActivityOptions.isCrossTask());
+ if (mEnterActivityOptions.isCrossTask()) {
+ mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
+ mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
+ }
+
+ if (!mIsEnterPostponed) {
+ startEnter();
+ }
+ }
+
+ public void postponeEnterTransition() {
+ mIsEnterPostponed = true;
+ }
+
+ public void startPostponedEnterTransition() {
+ if (mIsEnterPostponed) {
+ mIsEnterPostponed = false;
+ if (mEnterTransitionCoordinator != null) {
+ startEnter();
+ }
+ }
+ }
+
+ private void startEnter() {
+ if (mEnterTransitionCoordinator.isReturning()) {
+ if (mExitingToView != null) {
+ mEnterTransitionCoordinator.viewInstancesReady(mExitingFrom, mExitingTo,
+ mExitingToView);
+ } else {
+ mEnterTransitionCoordinator.namedViewsReady(mExitingFrom, mExitingTo);
+ }
+ } else {
+ mEnterTransitionCoordinator.namedViewsReady(null, null);
+ mEnteringNames = mEnterTransitionCoordinator.getAllSharedElementNames();
+ }
+
+ mExitingFrom = null;
+ mExitingTo = null;
+ mExitingToView = null;
+ mEnterActivityOptions = null;
+ }
+
+ public void onStop() {
+ restoreExitedViews();
+ if (mEnterTransitionCoordinator != null) {
+ mEnterTransitionCoordinator.stop();
+ mEnterTransitionCoordinator = null;
+ }
+ if (mReturnExitCoordinator != null) {
+ mReturnExitCoordinator.stop();
+ mReturnExitCoordinator = null;
+ }
+ }
+
+ public void onResume(Activity activity, boolean isTopOfTask) {
+ // After orientation change, the onResume can come in before the top Activity has
+ // left, so if the Activity is not top, wait a second for the top Activity to exit.
+ if (isTopOfTask || mEnterTransitionCoordinator == null) {
+ restoreExitedViews();
+ restoreReenteringViews();
+ } else {
+ activity.mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (mEnterTransitionCoordinator == null ||
+ mEnterTransitionCoordinator.isWaitingForRemoteExit()) {
+ restoreExitedViews();
+ restoreReenteringViews();
+ }
+ }
+ }, 1000);
+ }
+ }
+
+ public void clear() {
+ mEnteringNames = null;
+ mExitingFrom = null;
+ mExitingTo = null;
+ mExitingToView = null;
+ mCalledExitCoordinator = null;
+ mEnterTransitionCoordinator = null;
+ mEnterActivityOptions = null;
+ mExitTransitionCoordinators = null;
+ }
+
+ private void restoreExitedViews() {
+ if (mCalledExitCoordinator != null) {
+ mCalledExitCoordinator.resetViews();
+ mCalledExitCoordinator = null;
+ }
+ }
+
+ private void restoreReenteringViews() {
+ if (mEnterTransitionCoordinator != null && mEnterTransitionCoordinator.isReturning() &&
+ !mEnterTransitionCoordinator.isCrossTask()) {
+ mEnterTransitionCoordinator.forceViewsToAppear();
+ mExitingFrom = null;
+ mExitingTo = null;
+ mExitingToView = null;
+ }
+ }
+
+ public boolean startExitBackTransition(final Activity activity) {
+ if (mEnteringNames == null || mCalledExitCoordinator != null) {
+ return false;
+ } else {
+ if (!mHasExited) {
+ mHasExited = true;
+ Transition enterViewsTransition = null;
+ ViewGroup decor = null;
+ boolean delayExitBack = false;
+ if (mEnterTransitionCoordinator != null) {
+ enterViewsTransition = mEnterTransitionCoordinator.getEnterViewsTransition();
+ decor = mEnterTransitionCoordinator.getDecor();
+ delayExitBack = mEnterTransitionCoordinator.cancelEnter();
+ mEnterTransitionCoordinator = null;
+ if (enterViewsTransition != null && decor != null) {
+ enterViewsTransition.pause(decor);
+ }
+ }
+
+ mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
+ activity.getWindow(), activity.mEnterTransitionListener, mEnteringNames,
+ null, null, true);
+ if (enterViewsTransition != null && decor != null) {
+ enterViewsTransition.resume(decor);
+ }
+ if (delayExitBack && decor != null) {
+ final ViewGroup finalDecor = decor;
+ OneShotPreDrawListener.add(decor, () -> {
+ if (mReturnExitCoordinator != null) {
+ mReturnExitCoordinator.startExit(activity.mResultCode,
+ activity.mResultData);
+ }
+ });
+ } else {
+ mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData);
+ }
+ }
+ return true;
+ }
+ }
+
+ public boolean isTransitionRunning() {
+ // Note that *only* enter *or* exit will be running at any given time
+ if (mEnterTransitionCoordinator != null) {
+ if (mEnterTransitionCoordinator.isTransitionRunning()) {
+ return true;
+ }
+ }
+ if (mCalledExitCoordinator != null) {
+ if (mCalledExitCoordinator.isTransitionRunning()) {
+ return true;
+ }
+ }
+ if (mReturnExitCoordinator != null) {
+ if (mReturnExitCoordinator.isTransitionRunning()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void startExitOutTransition(Activity activity, Bundle options) {
+ mEnterTransitionCoordinator = null;
+ if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) ||
+ mExitTransitionCoordinators == null) {
+ return;
+ }
+ ActivityOptions activityOptions = new ActivityOptions(options);
+ if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
+ int key = activityOptions.getExitCoordinatorKey();
+ int index = mExitTransitionCoordinators.indexOfKey(key);
+ if (index >= 0) {
+ mCalledExitCoordinator = mExitTransitionCoordinators.valueAt(index).get();
+ mExitTransitionCoordinators.removeAt(index);
+ if (mCalledExitCoordinator != null) {
+ mExitingFrom = mCalledExitCoordinator.getAcceptedNames();
+ mExitingTo = mCalledExitCoordinator.getMappedNames();
+ mExitingToView = mCalledExitCoordinator.copyMappedViews();
+ mCalledExitCoordinator.startExit();
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/ActivityView.java b/android/app/ActivityView.java
new file mode 100644
index 00000000..9f1e9839
--- /dev/null
+++ b/android/app/ActivityView.java
@@ -0,0 +1,335 @@
+/**
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.hardware.input.InputManager;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import dalvik.system.CloseGuard;
+
+/**
+ * Activity container that allows launching activities into itself and does input forwarding.
+ * <p>Creation of this view is only allowed to callers who have
+ * {@link android.Manifest.permission#INJECT_EVENTS} permission.
+ * <p>Activity launching into this container is restricted by the same rules that apply to launching
+ * on VirtualDisplays.
+ * @hide
+ */
+public class ActivityView extends ViewGroup {
+
+ private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay";
+ private static final String TAG = "ActivityView";
+
+ private VirtualDisplay mVirtualDisplay;
+ private final SurfaceView mSurfaceView;
+ private Surface mSurface;
+
+ private final SurfaceCallback mSurfaceCallback;
+ private StateCallback mActivityViewCallback;
+
+ private IInputForwarder mInputForwarder;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mOpened; // Protected by mGuard.
+
+ public ActivityView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public ActivityView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyle */);
+ }
+
+ public ActivityView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mSurfaceView = new SurfaceView(context);
+ mSurfaceCallback = new SurfaceCallback();
+ mSurfaceView.getHolder().addCallback(mSurfaceCallback);
+ addView(mSurfaceView);
+
+ mOpened = true;
+ mGuard.open("release");
+ }
+
+ /** Callback that notifies when the container is ready or destroyed. */
+ public abstract static class StateCallback {
+ /**
+ * Called when the container is ready for launching activities. Calling
+ * {@link #startActivity(Intent)} prior to this callback will result in an
+ * {@link IllegalStateException}.
+ *
+ * @see #startActivity(Intent)
+ */
+ public abstract void onActivityViewReady(ActivityView view);
+ /**
+ * Called when the container can no longer launch activities. Calling
+ * {@link #startActivity(Intent)} after this callback will result in an
+ * {@link IllegalStateException}.
+ *
+ * @see #startActivity(Intent)
+ */
+ public abstract void onActivityViewDestroyed(ActivityView view);
+ }
+
+ /**
+ * Set the callback to be notified about state changes.
+ * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
+ * <p>Note: If the instance was ready prior to this call being made, then
+ * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within
+ * this method call.
+ *
+ * @param callback The callback to report events to.
+ *
+ * @see StateCallback
+ * @see #startActivity(Intent)
+ */
+ public void setCallback(StateCallback callback) {
+ mActivityViewCallback = callback;
+
+ if (mVirtualDisplay != null && mActivityViewCallback != null) {
+ mActivityViewCallback.onActivityViewReady(this);
+ }
+ }
+
+ /**
+ * Launch a new activity into this container.
+ * <p>Activity resolved by the provided {@link Intent} must have
+ * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
+ * launched here. Also, if activity is not owned by the owner of this container, it must allow
+ * embedding and the caller must have permission to embed.
+ * <p>Note: This class must finish initializing and
+ * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
+ * this method can be called.
+ *
+ * @param intent Intent used to launch an activity.
+ *
+ * @see StateCallback
+ * @see #startActivity(PendingIntent)
+ */
+ public void startActivity(@NonNull Intent intent) {
+ final ActivityOptions options = prepareActivityOptions();
+ getContext().startActivity(intent, options.toBundle());
+ }
+
+ /**
+ * Launch a new activity into this container.
+ * <p>Activity resolved by the provided {@link PendingIntent} must have
+ * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
+ * launched here. Also, if activity is not owned by the owner of this container, it must allow
+ * embedding and the caller must have permission to embed.
+ * <p>Note: This class must finish initializing and
+ * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
+ * this method can be called.
+ *
+ * @param pendingIntent Intent used to launch an activity.
+ *
+ * @see StateCallback
+ * @see #startActivity(Intent)
+ */
+ public void startActivity(@NonNull PendingIntent pendingIntent) {
+ final ActivityOptions options = prepareActivityOptions();
+ try {
+ pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
+ null /* onFinished */, null /* handler */, null /* requiredPermission */,
+ options.toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Check if container is ready to launch and create {@link ActivityOptions} to target the
+ * virtual display.
+ */
+ private ActivityOptions prepareActivityOptions() {
+ if (mVirtualDisplay == null) {
+ throw new IllegalStateException(
+ "Trying to start activity before ActivityView is ready.");
+ }
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
+ return options;
+ }
+
+ /**
+ * Release this container. Activity launching will no longer be permitted.
+ * <p>Note: Calling this method is allowed after
+ * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before
+ * {@link StateCallback#onActivityViewDestroyed(ActivityView)}.
+ *
+ * @see StateCallback
+ */
+ public void release() {
+ if (mVirtualDisplay == null) {
+ throw new IllegalStateException(
+ "Trying to release container that is not initialized.");
+ }
+ performRelease();
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return injectInputEvent(event) || super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ if (injectInputEvent(event)) {
+ return true;
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ private boolean injectInputEvent(InputEvent event) {
+ if (mInputForwarder != null) {
+ try {
+ return mInputForwarder.forwardEvent(event);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+ return false;
+ }
+
+ private class SurfaceCallback implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceCreated(SurfaceHolder surfaceHolder) {
+ mSurface = mSurfaceView.getHolder().getSurface();
+ if (mVirtualDisplay == null) {
+ initVirtualDisplay();
+ if (mVirtualDisplay != null && mActivityViewCallback != null) {
+ mActivityViewCallback.onActivityViewReady(ActivityView.this);
+ }
+ } else {
+ mVirtualDisplay.setSurface(surfaceHolder.getSurface());
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
+ if (mVirtualDisplay != null) {
+ mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+ mSurface.release();
+ mSurface = null;
+ if (mVirtualDisplay != null) {
+ mVirtualDisplay.setSurface(null);
+ }
+ }
+ }
+
+ private void initVirtualDisplay() {
+ if (mVirtualDisplay != null) {
+ throw new IllegalStateException("Trying to initialize for the second time.");
+ }
+
+ final int width = mSurfaceView.getWidth();
+ final int height = mSurfaceView.getHeight();
+ final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+ mVirtualDisplay = displayManager.createVirtualDisplay(
+ DISPLAY_NAME + "@" + System.identityHashCode(this),
+ width, height, getBaseDisplayDensity(), mSurface, 0 /* flags */);
+ if (mVirtualDisplay == null) {
+ Log.e(TAG, "Failed to initialize ActivityView");
+ return;
+ }
+
+ mInputForwarder = InputManager.getInstance().createInputForwarder(
+ mVirtualDisplay.getDisplay().getDisplayId());
+ }
+
+ private void performRelease() {
+ if (!mOpened) {
+ return;
+ }
+
+ mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
+
+ if (mInputForwarder != null) {
+ mInputForwarder = null;
+ }
+
+ final boolean displayReleased;
+ if (mVirtualDisplay != null) {
+ mVirtualDisplay.release();
+ mVirtualDisplay = null;
+ displayReleased = true;
+ } else {
+ displayReleased = false;
+ }
+
+ if (mSurface != null) {
+ mSurface.release();
+ mSurface = null;
+ }
+
+ if (displayReleased && mActivityViewCallback != null) {
+ mActivityViewCallback.onActivityViewDestroyed(this);
+ }
+
+ mGuard.close();
+ mOpened = false;
+ }
+
+ /** Get density of the hosting display. */
+ private int getBaseDisplayDensity() {
+ final WindowManager wm = mContext.getSystemService(WindowManager.class);
+ final DisplayMetrics metrics = new DisplayMetrics();
+ wm.getDefaultDisplay().getMetrics(metrics);
+ return metrics.densityDpi;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ performRelease();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/android/app/AlarmManager.java b/android/app/AlarmManager.java
new file mode 100644
index 00000000..2813e8b9
--- /dev/null
+++ b/android/app/AlarmManager.java
@@ -0,0 +1,1125 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import libcore.util.ZoneInfoDB;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class provides access to the system alarm services. These allow you
+ * to schedule your application to be run at some point in the future. When
+ * an alarm goes off, the {@link Intent} that had been registered for it
+ * is broadcast by the system, automatically starting the target application
+ * if it is not already running. Registered alarms are retained while the
+ * device is asleep (and can optionally wake the device up if they go off
+ * during that time), but will be cleared if it is turned off and rebooted.
+ *
+ * <p>The Alarm Manager holds a CPU wake lock as long as the alarm receiver's
+ * onReceive() method is executing. This guarantees that the phone will not sleep
+ * until you have finished handling the broadcast. Once onReceive() returns, the
+ * Alarm Manager releases this wake lock. This means that the phone will in some
+ * cases sleep as soon as your onReceive() method completes. If your alarm receiver
+ * called {@link android.content.Context#startService Context.startService()}, it
+ * is possible that the phone will sleep before the requested service is launched.
+ * To prevent this, your BroadcastReceiver and Service will need to implement a
+ * separate wake lock policy to ensure that the phone continues running until the
+ * service becomes available.
+ *
+ * <p><b>Note: The Alarm Manager is intended for cases where you want to have
+ * your application code run at a specific time, even if your application is
+ * not currently running. For normal timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b>
+ *
+ * <p class="caution"><strong>Note:</strong> Beginning with API 19
+ * ({@link android.os.Build.VERSION_CODES#KITKAT}) alarm delivery is inexact:
+ * the OS will shift alarms in order to minimize wakeups and battery use. There are
+ * new APIs to support applications which need strict delivery guarantees; see
+ * {@link #setWindow(int, long, long, PendingIntent)} and
+ * {@link #setExact(int, long, PendingIntent)}. Applications whose {@code targetSdkVersion}
+ * is earlier than API 19 will continue to see the previous behavior in which all
+ * alarms are delivered exactly when requested.
+ */
+@SystemService(Context.ALARM_SERVICE)
+public class AlarmManager {
+ private static final String TAG = "AlarmManager";
+
+ /** @hide */
+ @IntDef(prefix = { "RTC", "ELAPSED" }, value = {
+ RTC_WAKEUP,
+ RTC,
+ ELAPSED_REALTIME_WAKEUP,
+ ELAPSED_REALTIME,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AlarmType {}
+
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC), which will wake up the device when
+ * it goes off.
+ */
+ public static final int RTC_WAKEUP = 0;
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC). This alarm does not wake the
+ * device up; if it goes off while the device is asleep, it will not be
+ * delivered until the next time the device wakes up.
+ */
+ public static final int RTC = 1;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep),
+ * which will wake up the device when it goes off.
+ */
+ public static final int ELAPSED_REALTIME_WAKEUP = 2;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep).
+ * This alarm does not wake the device up; if it goes off while the device
+ * is asleep, it will not be delivered until the next time the device
+ * wakes up.
+ */
+ public static final int ELAPSED_REALTIME = 3;
+
+ /**
+ * Broadcast Action: Sent after the value returned by
+ * {@link #getNextAlarmClock()} has changed.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * It is only sent to registered receivers.</p>
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED =
+ "android.app.action.NEXT_ALARM_CLOCK_CHANGED";
+
+ /** @hide */
+ public static final long WINDOW_EXACT = 0;
+ /** @hide */
+ public static final long WINDOW_HEURISTIC = -1;
+
+ /**
+ * Flag for alarms: this is to be a stand-alone alarm, that should not be batched with
+ * other alarms.
+ * @hide
+ */
+ public static final int FLAG_STANDALONE = 1<<0;
+
+ /**
+ * Flag for alarms: this alarm would like to wake the device even if it is idle. This
+ * is, for example, an alarm for an alarm clock.
+ * @hide
+ */
+ public static final int FLAG_WAKE_FROM_IDLE = 1<<1;
+
+ /**
+ * Flag for alarms: this alarm would like to still execute even if the device is
+ * idle. This won't bring the device out of idle, just allow this specific alarm to
+ * run. Note that this means the actual time this alarm goes off can be inconsistent
+ * with the time of non-allow-while-idle alarms (it could go earlier than the time
+ * requested by another alarm).
+ *
+ * @hide
+ */
+ public static final int FLAG_ALLOW_WHILE_IDLE = 1<<2;
+
+ /**
+ * Flag for alarms: same as {@link #FLAG_ALLOW_WHILE_IDLE}, but doesn't have restrictions
+ * on how frequently it can be scheduled. Only available (and automatically applied) to
+ * system alarms.
+ *
+ * @hide
+ */
+ public static final int FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED = 1<<3;
+
+ /**
+ * Flag for alarms: this alarm marks the point where we would like to come out of idle
+ * mode. It may be moved by the alarm manager to match the first wake-from-idle alarm.
+ * Scheduling an alarm with this flag puts the alarm manager in to idle mode, where it
+ * avoids scheduling any further alarms until the marker alarm is executed.
+ * @hide
+ */
+ public static final int FLAG_IDLE_UNTIL = 1<<4;
+
+ private final IAlarmManager mService;
+ private final String mPackageName;
+ private final boolean mAlwaysExact;
+ private final int mTargetSdkVersion;
+ private final Handler mMainThreadHandler;
+
+ /**
+ * Direct-notification alarms: the requester must be running continuously from the
+ * time the alarm is set to the time it is delivered, or delivery will fail. Only
+ * one-shot alarms can be set using this mechanism, not repeating alarms.
+ */
+ public interface OnAlarmListener {
+ /**
+ * Callback method that is invoked by the system when the alarm time is reached.
+ */
+ public void onAlarm();
+ }
+
+ final class ListenerWrapper extends IAlarmListener.Stub implements Runnable {
+ final OnAlarmListener mListener;
+ Handler mHandler;
+ IAlarmCompleteListener mCompletion;
+
+ public ListenerWrapper(OnAlarmListener listener) {
+ mListener = listener;
+ }
+
+ public void setHandler(Handler h) {
+ mHandler = h;
+ }
+
+ public void cancel() {
+ try {
+ mService.remove(null, this);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+
+ synchronized (AlarmManager.class) {
+ if (sWrappers != null) {
+ sWrappers.remove(mListener);
+ }
+ }
+ }
+
+ @Override
+ public void doAlarm(IAlarmCompleteListener alarmManager) {
+ mCompletion = alarmManager;
+
+ // Remove this listener from the wrapper cache first; the server side
+ // already considers it gone
+ synchronized (AlarmManager.class) {
+ if (sWrappers != null) {
+ sWrappers.remove(mListener);
+ }
+ }
+
+ mHandler.post(this);
+ }
+
+ @Override
+ public void run() {
+ // Now deliver it to the app
+ try {
+ mListener.onAlarm();
+ } finally {
+ // No catch -- make sure to report completion to the system process,
+ // but continue to allow the exception to crash the app.
+
+ try {
+ mCompletion.alarmComplete(this);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to report completion to Alarm Manager!", e);
+ }
+ }
+ }
+ }
+
+ // Tracking of the OnAlarmListener -> wrapper mapping, for cancel() support.
+ // Access is synchronized on the AlarmManager class object.
+ private static ArrayMap<OnAlarmListener, ListenerWrapper> sWrappers;
+
+ /**
+ * package private on purpose
+ */
+ AlarmManager(IAlarmManager service, Context ctx) {
+ mService = service;
+
+ mPackageName = ctx.getPackageName();
+ mTargetSdkVersion = ctx.getApplicationInfo().targetSdkVersion;
+ mAlwaysExact = (mTargetSdkVersion < Build.VERSION_CODES.KITKAT);
+ mMainThreadHandler = new Handler(ctx.getMainLooper());
+ }
+
+ private long legacyExactLength() {
+ return (mAlwaysExact ? WINDOW_EXACT : WINDOW_HEURISTIC);
+ }
+
+ /**
+ * <p>Schedule an alarm. <b>Note: for timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use {@link android.os.Handler}.</b>
+ * If there is already an alarm scheduled for the same IntentSender, that previous
+ * alarm will first be canceled.
+ *
+ * <p>If the stated trigger time is in the past, the alarm will be triggered
+ * immediately. If there is already an alarm for this Intent
+ * scheduled (with the equality of two intents being defined by
+ * {@link Intent#filterEquals}), then it will be removed and replaced by
+ * this one.
+ *
+ * <p>
+ * The alarm is an Intent broadcast that goes to a broadcast receiver that
+ * you registered with {@link android.content.Context#registerReceiver}
+ * or through the &lt;receiver&gt; tag in an AndroidManifest.xml file.
+ *
+ * <p>
+ * Alarm intents are delivered with a data extra of type int called
+ * {@link Intent#EXTRA_ALARM_COUNT Intent.EXTRA_ALARM_COUNT} that indicates
+ * how many past alarm events have been accumulated into this intent
+ * broadcast. Recurring alarms that have gone undelivered because the
+ * phone was asleep may have a count greater than one when delivered.
+ *
+ * <div class="note">
+ * <p>
+ * <b>Note:</b> Beginning in API 19, the trigger time passed to this method
+ * is treated as inexact: the alarm will not be delivered before this time, but
+ * may be deferred and delivered some time later. The OS will use
+ * this policy in order to "batch" alarms together across the entire system,
+ * minimizing the number of times the device needs to "wake up" and minimizing
+ * battery use. In general, alarms scheduled in the near future will not
+ * be deferred as long as alarms scheduled far in the future.
+ *
+ * <p>
+ * With the new batching policy, delivery ordering guarantees are not as
+ * strong as they were previously. If the application sets multiple alarms,
+ * it is possible that these alarms' <em>actual</em> delivery ordering may not match
+ * the order of their <em>requested</em> delivery times. If your application has
+ * strong ordering requirements there are other APIs that you can use to get
+ * the necessary behavior; see {@link #setWindow(int, long, long, PendingIntent)}
+ * and {@link #setExact(int, long, PendingIntent)}.
+ *
+ * <p>
+ * Applications whose {@code targetSdkVersion} is before API 19 will
+ * continue to get the previous alarm behavior: all of their scheduled alarms
+ * will be treated as exact.
+ * </div>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #setExact
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
+ null, null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, PendingIntent)}. Rather than
+ * supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param tag string describing the alarm, used for logging and battery-use
+ * attribution
+ * @param listener {@link OnAlarmListener} instance whose
+ * {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * called when the alarm time is reached. A given OnAlarmListener instance can
+ * only be the target of a single pending alarm, just as a given PendingIntent
+ * can only be used with one alarm at a time.
+ * @param targetHandler {@link Handler} on which to execute the listener's onAlarm()
+ * callback, or {@code null} to run that callback on the main looper.
+ */
+ public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
+ Handler targetHandler) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule a repeating alarm. <b>Note: for timing operations (ticks,
+ * timeouts, etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b> If there is already an alarm scheduled
+ * for the same IntentSender, it will first be canceled.
+ *
+ * <p>Like {@link #set}, except you can also supply a period at which
+ * the alarm will automatically repeat. This alarm continues
+ * repeating until explicitly removed with {@link #cancel}. If the stated
+ * trigger time is in the past, the alarm will be triggered immediately, with an
+ * alarm count depending on how far in the past the trigger time is relative
+ * to the repeat interval.
+ *
+ * <p>If an alarm is delayed (by system sleep, for example, for non
+ * _WAKEUP alarm types), a skipped repeat will be delivered as soon as
+ * possible. After that, future alarms will be delivered according to the
+ * original schedule; they do not drift over time. For example, if you have
+ * set a recurring alarm for the top of every hour but the phone was asleep
+ * from 7:45 until 8:45, an alarm will be sent as soon as the phone awakens,
+ * then the next alarm will be sent at 9:00.
+ *
+ * <p>If your application wants to allow the delivery times to drift in
+ * order to guarantee that at least a certain time interval always elapses
+ * between alarms, then the approach to take is to use one-time alarms,
+ * scheduling the next one yourself when handling each alarm delivery.
+ *
+ * <p class="note">
+ * <b>Note:</b> as of API 19, all repeating alarms are inexact. If your
+ * application needs precise delivery times then it must use one-time
+ * exact alarms, rescheduling each time as described above. Legacy applications
+ * whose {@code targetSdkVersion} is earlier than API 19 will continue to have all
+ * of their alarms, including repeating alarms, treated as exact.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type).
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #set
+ * @see #setExact
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setRepeating(@AlarmType int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Schedule an alarm to be delivered within a given window of time. This method
+ * is similar to {@link #set(int, long, PendingIntent)}, but allows the
+ * application to precisely control the degree to which its delivery might be
+ * adjusted by the OS. This method allows an application to take advantage of the
+ * battery optimizations that arise from delivery batching even when it has
+ * modest timeliness requirements for its alarms.
+ *
+ * <p>
+ * This method can also be used to achieve strict ordering guarantees among
+ * multiple alarms by ensuring that the windows requested for each alarm do
+ * not intersect.
+ *
+ * <p>
+ * When precise delivery is not required, applications should use the standard
+ * {@link #set(int, long, PendingIntent)} method. This will give the OS the most
+ * flexibility to minimize wakeups and battery use. For alarms that must be delivered
+ * at precisely-specified times with no acceptable variation, applications can use
+ * {@link #setExact(int, long, PendingIntent)}.
+ *
+ * @param type type of alarm.
+ * @param windowStartMillis The earliest time, in milliseconds, that the alarm should
+ * be delivered, expressed in the appropriate clock's units (depending on the alarm
+ * type).
+ * @param windowLengthMillis The length of the requested delivery window,
+ * in milliseconds. The alarm will be delivered no later than this many
+ * milliseconds after {@code windowStartMillis}. Note that this parameter
+ * is a <i>duration,</i> not the timestamp of the end of the window.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setExact
+ * @see #setRepeating
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ PendingIntent operation) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ */
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ String tag, OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an alarm to be delivered precisely at the stated time.
+ *
+ * <p>
+ * This method is like {@link #set(int, long, PendingIntent)}, but does not permit
+ * the OS to adjust the delivery time. The alarm will be delivered as nearly as
+ * possible to the requested trigger time.
+ *
+ * <p>
+ * <b>Note:</b> only alarms for which there is a strong demand for exact-time
+ * delivery (such as an alarm clock ringing at the requested time) should be
+ * scheduled as exact. Applications are strongly discouraged from using exact
+ * alarms unnecessarily as they reduce the OS's ability to minimize battery use.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null,
+ null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #setExact(int, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ */
+ public void setExact(@AlarmType int type, long triggerAtMillis, String tag,
+ OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag,
+ targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an idle-until alarm, which will keep the alarm manager idle until
+ * the given time.
+ * @hide
+ */
+ public void setIdleUntil(@AlarmType int type, long triggerAtMillis, String tag,
+ OnAlarmListener listener, Handler targetHandler) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null,
+ listener, tag, targetHandler, null, null);
+ }
+
+ /**
+ * Schedule an alarm that represents an alarm clock.
+ *
+ * The system may choose to display information about this alarm to the user.
+ *
+ * <p>
+ * This method is like {@link #setExact(int, long, PendingIntent)}, but implies
+ * {@link #RTC_WAKEUP}.
+ *
+ * @param info
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #setExact
+ * @see #cancel
+ * @see #getNextAlarmClock()
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ */
+ public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+ setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,
+ null, null, null, null, info);
+ }
+
+ /** @hide */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, PendingIntent operation, WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, null, null,
+ null, workSource, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
+ * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @hide
+ */
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, String tag, OnAlarmListener listener, Handler targetHandler,
+ WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag,
+ targetHandler, workSource, null);
+ }
+
+ /**
+ * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
+ * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Handler, or on the application's main looper
+ * if {@code null} is passed as the {@code targetHandler} parameter.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, OnAlarmListener listener, Handler targetHandler,
+ WorkSource workSource) {
+ setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null,
+ targetHandler, workSource, null);
+ }
+
+ private void setImpl(@AlarmType int type, long triggerAtMillis, long windowMillis,
+ long intervalMillis, int flags, PendingIntent operation, final OnAlarmListener listener,
+ String listenerTag, Handler targetHandler, WorkSource workSource,
+ AlarmClockInfo alarmClock) {
+ if (triggerAtMillis < 0) {
+ /* NOTYET
+ if (mAlwaysExact) {
+ // Fatal error for KLP+ apps to use negative trigger times
+ throw new IllegalArgumentException("Invalid alarm trigger time "
+ + triggerAtMillis);
+ }
+ */
+ triggerAtMillis = 0;
+ }
+
+ ListenerWrapper recipientWrapper = null;
+ if (listener != null) {
+ synchronized (AlarmManager.class) {
+ if (sWrappers == null) {
+ sWrappers = new ArrayMap<OnAlarmListener, ListenerWrapper>();
+ }
+
+ recipientWrapper = sWrappers.get(listener);
+ // no existing wrapper => build a new one
+ if (recipientWrapper == null) {
+ recipientWrapper = new ListenerWrapper(listener);
+ sWrappers.put(listener, recipientWrapper);
+ }
+ }
+
+ final Handler handler = (targetHandler != null) ? targetHandler : mMainThreadHandler;
+ recipientWrapper.setHandler(handler);
+ }
+
+ try {
+ mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags,
+ operation, recipientWrapper, listenerTag, workSource, alarmClock);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_FIFTEEN_MINUTES = 15 * 60 * 1000;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HALF_HOUR = 2*INTERVAL_FIFTEEN_MINUTES;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HOUR = 2*INTERVAL_HALF_HOUR;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_HALF_DAY = 12*INTERVAL_HOUR;
+
+ /**
+ * Available inexact recurrence interval recognized by
+ * {@link #setInexactRepeating(int, long, long, PendingIntent)}
+ * when running on Android prior to API 19.
+ */
+ public static final long INTERVAL_DAY = 2*INTERVAL_HALF_DAY;
+
+ /**
+ * Schedule a repeating alarm that has inexact trigger time requirements;
+ * for example, an alarm that repeats every hour, but not necessarily at
+ * the top of every hour. These alarms are more power-efficient than
+ * the strict recurrences traditionally supplied by {@link #setRepeating}, since the
+ * system can adjust alarms' delivery times to cause them to fire simultaneously,
+ * avoiding waking the device from sleep more than necessary.
+ *
+ * <p>Your alarm's first trigger will not be before the requested time,
+ * but it might not occur for almost a full interval after that time. In
+ * addition, while the overall period of the repeating alarm will be as
+ * requested, the time between any two successive firings of the alarm
+ * may vary. If your application demands very low jitter, use
+ * one-shot alarms with an appropriate window instead; see {@link
+ * #setWindow(int, long, long, PendingIntent)} and
+ * {@link #setExact(int, long, PendingIntent)}.
+ *
+ * <p class="note">
+ * As of API 19, all repeating alarms are inexact. Because this method has
+ * been available since API 3, your application can safely call it and be
+ * assured that it will get similar behavior on both current and older versions
+ * of Android.
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type). This
+ * is inexact: the alarm will not fire before this time, but there may be a
+ * delay of almost an entire alarm interval before the first invocation of
+ * the alarm.
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm. Prior to API 19, if this is one of INTERVAL_FIFTEEN_MINUTES,
+ * INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY
+ * then the alarm will be phase-aligned with other alarms to reduce the
+ * number of wakeups. Otherwise, the alarm will be set as though the
+ * application had called {@link #setRepeating}. As of API 19, all repeating
+ * alarms will be inexact and subject to batching with other alarms regardless
+ * of their stated repeat interval.
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #set
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ * @see #INTERVAL_FIFTEEN_MINUTES
+ * @see #INTERVAL_HALF_HOUR
+ * @see #INTERVAL_HOUR
+ * @see #INTERVAL_HALF_DAY
+ * @see #INTERVAL_DAY
+ */
+ public void setInexactRepeating(@AlarmType int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null,
+ null, null, null, null);
+ }
+
+ /**
+ * Like {@link #set(int, long, PendingIntent)}, but this alarm will be allowed to execute
+ * even when the system is in low-power idle modes. This type of alarm must <b>only</b>
+ * be used for situations where it is actually required that the alarm go off while in
+ * idle -- a reasonable example would be for a calendar notification that should make a
+ * sound so the user is aware of it. When the alarm is dispatched, the app will also be
+ * added to the system's temporary whitelist for approximately 10 seconds to allow that
+ * application to acquire further wake locks in which to complete its work.</p>
+ *
+ * <p>These alarms can significantly impact the power use
+ * of the device when idle (and thus cause significant battery blame to the app scheduling
+ * them), so they should be used with care. To reduce abuse, there are restrictions on how
+ * frequently these alarms will go off for a particular application.
+ * Under normal system operation, it will not dispatch these
+ * alarms more than about every minute (at which point every such pending alarm is
+ * dispatched); when in low-power idle modes this duration may be significantly longer,
+ * such as 15 minutes.</p>
+ *
+ * <p>Unlike other alarms, the system is free to reschedule this type of alarm to happen
+ * out of order with any other alarms, even those from the same app. This will clearly happen
+ * when the device is idle (since this alarm can go off while idle, when any other alarms
+ * from the app will be held until later), but may also happen even when not idle.</p>
+ *
+ * <p>Regardless of the app's target SDK version, this call always allows batching of the
+ * alarm.</p>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set(int, long, PendingIntent)
+ * @see #setExactAndAllowWhileIdle
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+ PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
+ operation, null, null, null, null, null);
+ }
+
+ /**
+ * Like {@link #setExact(int, long, PendingIntent)}, but this alarm will be allowed to execute
+ * even when the system is in low-power idle modes. If you don't need exact scheduling of
+ * the alarm but still need to execute while idle, consider using
+ * {@link #setAndAllowWhileIdle}. This type of alarm must <b>only</b>
+ * be used for situations where it is actually required that the alarm go off while in
+ * idle -- a reasonable example would be for a calendar notification that should make a
+ * sound so the user is aware of it. When the alarm is dispatched, the app will also be
+ * added to the system's temporary whitelist for approximately 10 seconds to allow that
+ * application to acquire further wake locks in which to complete its work.</p>
+ *
+ * <p>These alarms can significantly impact the power use
+ * of the device when idle (and thus cause significant battery blame to the app scheduling
+ * them), so they should be used with care. To reduce abuse, there are restrictions on how
+ * frequently these alarms will go off for a particular application.
+ * Under normal system operation, it will not dispatch these
+ * alarms more than about every minute (at which point every such pending alarm is
+ * dispatched); when in low-power idle modes this duration may be significantly longer,
+ * such as 15 minutes.</p>
+ *
+ * <p>Unlike other alarms, the system is free to reschedule this type of alarm to happen
+ * out of order with any other alarms, even those from the same app. This will clearly happen
+ * when the device is idle (since this alarm can go off while idle, when any other alarms
+ * from the app will be held until later), but may also happen even when not idle.
+ * Note that the OS will allow itself more flexibility for scheduling these alarms than
+ * regular exact alarms, since the application has opted into this behavior. When the
+ * device is idle it may take even more liberties with scheduling in order to optimize
+ * for battery life.</p>
+ *
+ * @param type type of alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
+ * @param operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see #set
+ * @see #setRepeating
+ * @see #setWindow
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+ PendingIntent operation) {
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,
+ null, null, null, null, null);
+ }
+
+ /**
+ * Remove any alarms with a matching {@link Intent}.
+ * Any alarm, of any type, whose Intent matches this one (as defined by
+ * {@link Intent#filterEquals}), will be canceled.
+ *
+ * @param operation IntentSender which matches a previously added
+ * IntentSender. This parameter must not be {@code null}.
+ *
+ * @see #set
+ */
+ public void cancel(PendingIntent operation) {
+ if (operation == null) {
+ final String msg = "cancel() called with a null PendingIntent";
+ if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
+ throw new NullPointerException(msg);
+ } else {
+ Log.e(TAG, msg);
+ return;
+ }
+ }
+
+ try {
+ mService.remove(operation, null);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove any alarm scheduled to be delivered to the given {@link OnAlarmListener}.
+ *
+ * @param listener OnAlarmListener instance that is the target of a currently-set alarm.
+ */
+ public void cancel(OnAlarmListener listener) {
+ if (listener == null) {
+ throw new NullPointerException("cancel() called with a null OnAlarmListener");
+ }
+
+ ListenerWrapper wrapper = null;
+ synchronized (AlarmManager.class) {
+ if (sWrappers != null) {
+ wrapper = sWrappers.get(listener);
+ }
+ }
+
+ if (wrapper == null) {
+ Log.w(TAG, "Unrecognized alarm listener " + listener);
+ return;
+ }
+
+ wrapper.cancel();
+ }
+
+ /**
+ * Set the system wall clock time.
+ * Requires the permission android.permission.SET_TIME.
+ *
+ * @param millis time in milliseconds since the Epoch
+ */
+ public void setTime(long millis) {
+ try {
+ mService.setTime(millis);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the system's persistent default time zone. This is the time zone for all apps, even
+ * after a reboot. Use {@link java.util.TimeZone#setDefault} if you just want to change the
+ * time zone within your app, and even then prefer to pass an explicit
+ * {@link java.util.TimeZone} to APIs that require it rather than changing the time zone for
+ * all threads.
+ *
+ * <p> On android M and above, it is an error to pass in a non-Olson timezone to this
+ * function. Note that this is a bad idea on all Android releases because POSIX and
+ * the {@code TimeZone} class have opposite interpretations of {@code '+'} and {@code '-'}
+ * in the same non-Olson ID.
+ *
+ * @param timeZone one of the Olson ids from the list returned by
+ * {@link java.util.TimeZone#getAvailableIDs}
+ */
+ public void setTimeZone(String timeZone) {
+ if (TextUtils.isEmpty(timeZone)) {
+ return;
+ }
+
+ // Reject this timezone if it isn't an Olson zone we recognize.
+ if (mTargetSdkVersion >= Build.VERSION_CODES.M) {
+ boolean hasTimeZone = false;
+ try {
+ hasTimeZone = ZoneInfoDB.getInstance().hasTimeZone(timeZone);
+ } catch (IOException ignored) {
+ }
+
+ if (!hasTimeZone) {
+ throw new IllegalArgumentException("Timezone: " + timeZone + " is not an Olson ID");
+ }
+ }
+
+ try {
+ mService.setTimeZone(timeZone);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public long getNextWakeFromIdleTime() {
+ try {
+ return mService.getNextWakeFromIdleTime();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets information about the next alarm clock currently scheduled.
+ *
+ * The alarm clocks considered are those scheduled by any application
+ * using the {@link #setAlarmClock} method.
+ *
+ * @return An {@link AlarmClockInfo} object describing the next upcoming alarm
+ * clock event that will occur. If there are no alarm clock events currently
+ * scheduled, this method will return {@code null}.
+ *
+ * @see #setAlarmClock
+ * @see AlarmClockInfo
+ * @see #ACTION_NEXT_ALARM_CLOCK_CHANGED
+ */
+ public AlarmClockInfo getNextAlarmClock() {
+ return getNextAlarmClock(UserHandle.myUserId());
+ }
+
+ /**
+ * Gets information about the next alarm clock currently scheduled.
+ *
+ * The alarm clocks considered are those scheduled by any application
+ * using the {@link #setAlarmClock} method within the given user.
+ *
+ * @return An {@link AlarmClockInfo} object describing the next upcoming alarm
+ * clock event that will occur within the given user. If there are no alarm clock
+ * events currently scheduled in that user, this method will return {@code null}.
+ *
+ * @see #setAlarmClock
+ * @see AlarmClockInfo
+ * @see #ACTION_NEXT_ALARM_CLOCK_CHANGED
+ *
+ * @hide
+ */
+ public AlarmClockInfo getNextAlarmClock(int userId) {
+ try {
+ return mService.getNextAlarmClock(userId);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * An immutable description of a scheduled "alarm clock" event.
+ *
+ * @see AlarmManager#setAlarmClock
+ * @see AlarmManager#getNextAlarmClock
+ */
+ public static final class AlarmClockInfo implements Parcelable {
+
+ private final long mTriggerTime;
+ private final PendingIntent mShowIntent;
+
+ /**
+ * Creates a new alarm clock description.
+ *
+ * @param triggerTime time at which the underlying alarm is triggered in wall time
+ * milliseconds since the epoch
+ * @param showIntent an intent that can be used to show or edit details of
+ * the alarm clock.
+ */
+ public AlarmClockInfo(long triggerTime, PendingIntent showIntent) {
+ mTriggerTime = triggerTime;
+ mShowIntent = showIntent;
+ }
+
+ /**
+ * Use the {@link #CREATOR}
+ * @hide
+ */
+ AlarmClockInfo(Parcel in) {
+ mTriggerTime = in.readLong();
+ mShowIntent = in.readParcelable(PendingIntent.class.getClassLoader());
+ }
+
+ /**
+ * Returns the time at which the alarm is going to trigger.
+ *
+ * This value is UTC wall clock time in milliseconds, as returned by
+ * {@link System#currentTimeMillis()} for example.
+ */
+ public long getTriggerTime() {
+ return mTriggerTime;
+ }
+
+ /**
+ * Returns an intent that can be used to show or edit details of the alarm clock in
+ * the application that scheduled it.
+ *
+ * <p class="note">Beware that any application can retrieve and send this intent,
+ * potentially with additional fields filled in. See
+ * {@link PendingIntent#send(android.content.Context, int, android.content.Intent)
+ * PendingIntent.send()} and {@link android.content.Intent#fillIn Intent.fillIn()}
+ * for details.
+ */
+ public PendingIntent getShowIntent() {
+ return mShowIntent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mTriggerTime);
+ dest.writeParcelable(mShowIntent, flags);
+ }
+
+ public static final Creator<AlarmClockInfo> CREATOR = new Creator<AlarmClockInfo>() {
+ @Override
+ public AlarmClockInfo createFromParcel(Parcel in) {
+ return new AlarmClockInfo(in);
+ }
+
+ @Override
+ public AlarmClockInfo[] newArray(int size) {
+ return new AlarmClockInfo[size];
+ }
+ };
+ }
+}
diff --git a/android/app/AlertDialog.java b/android/app/AlertDialog.java
new file mode 100644
index 00000000..a44bd030
--- /dev/null
+++ b/android/app/AlertDialog.java
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.internal.app.AlertController;
+
+import android.annotation.ArrayRes;
+import android.annotation.AttrRes;
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.ResourceId;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import com.android.internal.R;
+
+/**
+ * A subclass of Dialog that can display one, two or three buttons. If you only want to
+ * display a String in this dialog box, use the setMessage() method. If you
+ * want to display a more complex view, look up the FrameLayout called "custom"
+ * and add your view to it:
+ *
+ * <pre>
+ * FrameLayout fl = findViewById(android.R.id.custom);
+ * fl.addView(myView, new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+ * </pre>
+ *
+ * <p>The AlertDialog class takes care of automatically setting
+ * {@link WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM} for you based on whether
+ * any views in the dialog return true from {@link View#onCheckIsTextEditor()
+ * View.onCheckIsTextEditor()}. Generally you want this set for a Dialog
+ * without text editors, so that it will be placed on top of the current
+ * input method UI. You can modify this behavior by forcing the flag to your
+ * desired mode after calling {@link #onCreate}.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating dialogs, read the
+ * <a href="{@docRoot}guide/topics/ui/dialogs.html">Dialogs</a> developer guide.</p>
+ * </div>
+ */
+public class AlertDialog extends Dialog implements DialogInterface {
+ private AlertController mAlert;
+
+ /**
+ * Special theme constant for {@link #AlertDialog(Context, int)}: use
+ * the traditional (pre-Holo) alert dialog theme.
+ *
+ * @deprecated Use {@link android.R.style#Theme_Material_Dialog_Alert}.
+ */
+ @Deprecated
+ public static final int THEME_TRADITIONAL = 1;
+
+ /**
+ * Special theme constant for {@link #AlertDialog(Context, int)}: use
+ * the holographic alert theme with a dark background.
+ *
+ * @deprecated Use {@link android.R.style#Theme_Material_Dialog_Alert}.
+ */
+ @Deprecated
+ public static final int THEME_HOLO_DARK = 2;
+
+ /**
+ * Special theme constant for {@link #AlertDialog(Context, int)}: use
+ * the holographic alert theme with a light background.
+ *
+ * @deprecated Use {@link android.R.style#Theme_Material_Light_Dialog_Alert}.
+ */
+ @Deprecated
+ public static final int THEME_HOLO_LIGHT = 3;
+
+ /**
+ * Special theme constant for {@link #AlertDialog(Context, int)}: use
+ * the device's default alert theme with a dark background.
+ *
+ * @deprecated Use {@link android.R.style#Theme_DeviceDefault_Dialog_Alert}.
+ */
+ @Deprecated
+ public static final int THEME_DEVICE_DEFAULT_DARK = 4;
+
+ /**
+ * Special theme constant for {@link #AlertDialog(Context, int)}: use
+ * the device's default alert theme with a light background.
+ *
+ * @deprecated Use {@link android.R.style#Theme_DeviceDefault_Light_Dialog_Alert}.
+ */
+ @Deprecated
+ public static final int THEME_DEVICE_DEFAULT_LIGHT = 5;
+
+ /**
+ * No layout hint.
+ * @hide
+ */
+ public static final int LAYOUT_HINT_NONE = 0;
+
+ /**
+ * Hint layout to the side.
+ * @hide
+ */
+ public static final int LAYOUT_HINT_SIDE = 1;
+
+ /**
+ * Creates an alert dialog that uses the default alert dialog theme.
+ * <p>
+ * The default alert dialog theme is defined by
+ * {@link android.R.attr#alertDialogTheme} within the parent
+ * {@code context}'s theme.
+ *
+ * @param context the parent context
+ * @see android.R.styleable#Theme_alertDialogTheme
+ */
+ protected AlertDialog(Context context) {
+ this(context, 0);
+ }
+
+ /**
+ * Creates an alert dialog that uses the default alert dialog theme and a
+ * custom cancel listener.
+ * <p>
+ * This is functionally identical to:
+ * <pre>
+ * AlertDialog dialog = new AlertDialog(context);
+ * alertDialog.setCancelable(cancelable);
+ * alertDialog.setOnCancelListener(cancelListener);
+ * </pre>
+ * <p>
+ * The default alert dialog theme is defined by
+ * {@link android.R.attr#alertDialogTheme} within the parent
+ * {@code context}'s theme.
+ *
+ * @param context the parent context
+ * @see android.R.styleable#Theme_alertDialogTheme
+ */
+ protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
+ this(context, 0);
+
+ setCancelable(cancelable);
+ setOnCancelListener(cancelListener);
+ }
+
+ /**
+ * Creates an alert dialog that uses an explicit theme resource.
+ * <p>
+ * The specified theme resource ({@code themeResId}) is applied on top of
+ * the parent {@code context}'s theme. It may be specified as a style
+ * resource containing a fully-populated theme, such as
+ * {@link android.R.style#Theme_Material_Dialog}, to replace all attributes
+ * in the parent {@code context}'s theme including primary and accent
+ * colors.
+ * <p>
+ * To preserve attributes such as primary and accent colors, the
+ * {@code themeResId} may instead be specified as an overlay theme such as
+ * {@link android.R.style#ThemeOverlay_Material_Dialog}. This will override
+ * only the window attributes necessary to style the alert window as a
+ * dialog.
+ * <p>
+ * Alternatively, the {@code themeResId} may be specified as {@code 0} to
+ * use the parent {@code context}'s resolved value for
+ * {@link android.R.attr#alertDialogTheme}.
+ *
+ * @param context the parent context
+ * @param themeResId the resource ID of the theme against which to inflate
+ * this dialog, or {@code 0} to use the parent
+ * {@code context}'s default alert dialog theme
+ * @see android.R.styleable#Theme_alertDialogTheme
+ */
+ protected AlertDialog(Context context, @StyleRes int themeResId) {
+ this(context, themeResId, true);
+ }
+
+ AlertDialog(Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
+ super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0,
+ createContextThemeWrapper);
+
+ mWindow.alwaysReadCloseOnTouchAttr();
+ mAlert = AlertController.create(getContext(), this, getWindow());
+ }
+
+ static @StyleRes int resolveDialogTheme(Context context, @StyleRes int themeResId) {
+ if (themeResId == THEME_TRADITIONAL) {
+ return R.style.Theme_Dialog_Alert;
+ } else if (themeResId == THEME_HOLO_DARK) {
+ return R.style.Theme_Holo_Dialog_Alert;
+ } else if (themeResId == THEME_HOLO_LIGHT) {
+ return R.style.Theme_Holo_Light_Dialog_Alert;
+ } else if (themeResId == THEME_DEVICE_DEFAULT_DARK) {
+ return R.style.Theme_DeviceDefault_Dialog_Alert;
+ } else if (themeResId == THEME_DEVICE_DEFAULT_LIGHT) {
+ return R.style.Theme_DeviceDefault_Light_Dialog_Alert;
+ } else if (ResourceId.isValid(themeResId)) {
+ // start of real resource IDs.
+ return themeResId;
+ } else {
+ final TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true);
+ return outValue.resourceId;
+ }
+ }
+
+ /**
+ * Gets one of the buttons used in the dialog. Returns null if the specified
+ * button does not exist or the dialog has not yet been fully created (for
+ * example, via {@link #show()} or {@link #create()}).
+ *
+ * @param whichButton The identifier of the button that should be returned.
+ * For example, this can be
+ * {@link DialogInterface#BUTTON_POSITIVE}.
+ * @return The button from the dialog, or null if a button does not exist.
+ */
+ public Button getButton(int whichButton) {
+ return mAlert.getButton(whichButton);
+ }
+
+ /**
+ * Gets the list view used in the dialog.
+ *
+ * @return The {@link ListView} from the dialog.
+ */
+ public ListView getListView() {
+ return mAlert.getListView();
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ mAlert.setTitle(title);
+ }
+
+ /**
+ * @see Builder#setCustomTitle(View)
+ */
+ public void setCustomTitle(View customTitleView) {
+ mAlert.setCustomTitle(customTitleView);
+ }
+
+ public void setMessage(CharSequence message) {
+ mAlert.setMessage(message);
+ }
+
+ /**
+ * Set the view to display in that dialog.
+ */
+ public void setView(View view) {
+ mAlert.setView(view);
+ }
+
+ /**
+ * Set the view to display in that dialog, specifying the spacing to appear around that
+ * view.
+ *
+ * @param view The view to show in the content area of the dialog
+ * @param viewSpacingLeft Extra space to appear to the left of {@code view}
+ * @param viewSpacingTop Extra space to appear above {@code view}
+ * @param viewSpacingRight Extra space to appear to the right of {@code view}
+ * @param viewSpacingBottom Extra space to appear below {@code view}
+ */
+ public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight,
+ int viewSpacingBottom) {
+ mAlert.setView(view, viewSpacingLeft, viewSpacingTop, viewSpacingRight, viewSpacingBottom);
+ }
+
+ /**
+ * Internal api to allow hinting for the best button panel layout.
+ * @hide
+ */
+ void setButtonPanelLayoutHint(int layoutHint) {
+ mAlert.setButtonPanelLayoutHint(layoutHint);
+ }
+
+ /**
+ * Set a message to be sent when a button is pressed.
+ *
+ * @param whichButton Which button to set the message for, can be one of
+ * {@link DialogInterface#BUTTON_POSITIVE},
+ * {@link DialogInterface#BUTTON_NEGATIVE}, or
+ * {@link DialogInterface#BUTTON_NEUTRAL}
+ * @param text The text to display in positive button.
+ * @param msg The {@link Message} to be sent when clicked.
+ */
+ public void setButton(int whichButton, CharSequence text, Message msg) {
+ mAlert.setButton(whichButton, text, null, msg);
+ }
+
+ /**
+ * Set a listener to be invoked when the positive button of the dialog is pressed.
+ *
+ * @param whichButton Which button to set the listener on, can be one of
+ * {@link DialogInterface#BUTTON_POSITIVE},
+ * {@link DialogInterface#BUTTON_NEGATIVE}, or
+ * {@link DialogInterface#BUTTON_NEUTRAL}
+ * @param text The text to display in positive button.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public void setButton(int whichButton, CharSequence text, OnClickListener listener) {
+ mAlert.setButton(whichButton, text, listener, null);
+ }
+
+ /**
+ * @deprecated Use {@link #setButton(int, CharSequence, Message)} with
+ * {@link DialogInterface#BUTTON_POSITIVE}.
+ */
+ @Deprecated
+ public void setButton(CharSequence text, Message msg) {
+ setButton(BUTTON_POSITIVE, text, msg);
+ }
+
+ /**
+ * @deprecated Use {@link #setButton(int, CharSequence, Message)} with
+ * {@link DialogInterface#BUTTON_NEGATIVE}.
+ */
+ @Deprecated
+ public void setButton2(CharSequence text, Message msg) {
+ setButton(BUTTON_NEGATIVE, text, msg);
+ }
+
+ /**
+ * @deprecated Use {@link #setButton(int, CharSequence, Message)} with
+ * {@link DialogInterface#BUTTON_NEUTRAL}.
+ */
+ @Deprecated
+ public void setButton3(CharSequence text, Message msg) {
+ setButton(BUTTON_NEUTRAL, text, msg);
+ }
+
+ /**
+ * Set a listener to be invoked when button 1 of the dialog is pressed.
+ *
+ * @param text The text to display in button 1.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ * @deprecated Use
+ * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)}
+ * with {@link DialogInterface#BUTTON_POSITIVE}
+ */
+ @Deprecated
+ public void setButton(CharSequence text, final OnClickListener listener) {
+ setButton(BUTTON_POSITIVE, text, listener);
+ }
+
+ /**
+ * Set a listener to be invoked when button 2 of the dialog is pressed.
+ * @param text The text to display in button 2.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ * @deprecated Use
+ * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)}
+ * with {@link DialogInterface#BUTTON_NEGATIVE}
+ */
+ @Deprecated
+ public void setButton2(CharSequence text, final OnClickListener listener) {
+ setButton(BUTTON_NEGATIVE, text, listener);
+ }
+
+ /**
+ * Set a listener to be invoked when button 3 of the dialog is pressed.
+ * @param text The text to display in button 3.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ * @deprecated Use
+ * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)}
+ * with {@link DialogInterface#BUTTON_POSITIVE}
+ */
+ @Deprecated
+ public void setButton3(CharSequence text, final OnClickListener listener) {
+ setButton(BUTTON_NEUTRAL, text, listener);
+ }
+
+ /**
+ * Set resId to 0 if you don't want an icon.
+ * @param resId the resourceId of the drawable to use as the icon or 0
+ * if you don't want an icon.
+ */
+ public void setIcon(@DrawableRes int resId) {
+ mAlert.setIcon(resId);
+ }
+
+ public void setIcon(Drawable icon) {
+ mAlert.setIcon(icon);
+ }
+
+ /**
+ * Set an icon as supplied by a theme attribute. e.g. android.R.attr.alertDialogIcon
+ *
+ * @param attrId ID of a theme attribute that points to a drawable resource.
+ */
+ public void setIconAttribute(@AttrRes int attrId) {
+ TypedValue out = new TypedValue();
+ mContext.getTheme().resolveAttribute(attrId, out, true);
+ mAlert.setIcon(out.resourceId);
+ }
+
+ public void setInverseBackgroundForced(boolean forceInverseBackground) {
+ mAlert.setInverseBackgroundForced(forceInverseBackground);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAlert.installContent();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyDown(keyCode, event)) return true;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyUp(keyCode, event)) return true;
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public static class Builder {
+ private final AlertController.AlertParams P;
+
+ /**
+ * Creates a builder for an alert dialog that uses the default alert
+ * dialog theme.
+ * <p>
+ * The default alert dialog theme is defined by
+ * {@link android.R.attr#alertDialogTheme} within the parent
+ * {@code context}'s theme.
+ *
+ * @param context the parent context
+ */
+ public Builder(Context context) {
+ this(context, resolveDialogTheme(context, ResourceId.ID_NULL));
+ }
+
+ /**
+ * Creates a builder for an alert dialog that uses an explicit theme
+ * resource.
+ * <p>
+ * The specified theme resource ({@code themeResId}) is applied on top
+ * of the parent {@code context}'s theme. It may be specified as a
+ * style resource containing a fully-populated theme, such as
+ * {@link android.R.style#Theme_Material_Dialog}, to replace all
+ * attributes in the parent {@code context}'s theme including primary
+ * and accent colors.
+ * <p>
+ * To preserve attributes such as primary and accent colors, the
+ * {@code themeResId} may instead be specified as an overlay theme such
+ * as {@link android.R.style#ThemeOverlay_Material_Dialog}. This will
+ * override only the window attributes necessary to style the alert
+ * window as a dialog.
+ * <p>
+ * Alternatively, the {@code themeResId} may be specified as {@code 0}
+ * to use the parent {@code context}'s resolved value for
+ * {@link android.R.attr#alertDialogTheme}.
+ *
+ * @param context the parent context
+ * @param themeResId the resource ID of the theme against which to inflate
+ * this dialog, or {@code 0} to use the parent
+ * {@code context}'s default alert dialog theme
+ */
+ public Builder(Context context, int themeResId) {
+ P = new AlertController.AlertParams(new ContextThemeWrapper(
+ context, resolveDialogTheme(context, themeResId)));
+ }
+
+ /**
+ * Returns a {@link Context} with the appropriate theme for dialogs created by this Builder.
+ * Applications should use this Context for obtaining LayoutInflaters for inflating views
+ * that will be used in the resulting dialogs, as it will cause views to be inflated with
+ * the correct theme.
+ *
+ * @return A Context for built Dialogs.
+ */
+ public Context getContext() {
+ return P.mContext;
+ }
+
+ /**
+ * Set the title using the given resource id.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setTitle(@StringRes int titleId) {
+ P.mTitle = P.mContext.getText(titleId);
+ return this;
+ }
+
+ /**
+ * Set the title displayed in the {@link Dialog}.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setTitle(CharSequence title) {
+ P.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Set the title using the custom view {@code customTitleView}.
+ * <p>
+ * The methods {@link #setTitle(int)} and {@link #setIcon(int)} should
+ * be sufficient for most titles, but this is provided if the title
+ * needs more customization. Using this will replace the title and icon
+ * set via the other methods.
+ * <p>
+ * <strong>Note:</strong> To ensure consistent styling, the custom view
+ * should be inflated or constructed using the alert dialog's themed
+ * context obtained via {@link #getContext()}.
+ *
+ * @param customTitleView the custom view to use as the title
+ * @return this Builder object to allow for chaining of calls to set
+ * methods
+ */
+ public Builder setCustomTitle(View customTitleView) {
+ P.mCustomTitleView = customTitleView;
+ return this;
+ }
+
+ /**
+ * Set the message to display using the given resource id.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setMessage(@StringRes int messageId) {
+ P.mMessage = P.mContext.getText(messageId);
+ return this;
+ }
+
+ /**
+ * Set the message to display.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setMessage(CharSequence message) {
+ P.mMessage = message;
+ return this;
+ }
+
+ /**
+ * Set the resource id of the {@link Drawable} to be used in the title.
+ * <p>
+ * Takes precedence over values set using {@link #setIcon(Drawable)}.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setIcon(@DrawableRes int iconId) {
+ P.mIconId = iconId;
+ return this;
+ }
+
+ /**
+ * Set the {@link Drawable} to be used in the title.
+ * <p>
+ * <strong>Note:</strong> To ensure consistent styling, the drawable
+ * should be inflated or constructed using the alert dialog's themed
+ * context obtained via {@link #getContext()}.
+ *
+ * @return this Builder object to allow for chaining of calls to set
+ * methods
+ */
+ public Builder setIcon(Drawable icon) {
+ P.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set an icon as supplied by a theme attribute. e.g.
+ * {@link android.R.attr#alertDialogIcon}.
+ * <p>
+ * Takes precedence over values set using {@link #setIcon(int)} or
+ * {@link #setIcon(Drawable)}.
+ *
+ * @param attrId ID of a theme attribute that points to a drawable resource.
+ */
+ public Builder setIconAttribute(@AttrRes int attrId) {
+ TypedValue out = new TypedValue();
+ P.mContext.getTheme().resolveAttribute(attrId, out, true);
+ P.mIconId = out.resourceId;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the positive button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the positive button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setPositiveButton(@StringRes int textId, final OnClickListener listener) {
+ P.mPositiveButtonText = P.mContext.getText(textId);
+ P.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the positive button of the dialog is pressed.
+ * @param text The text to display in the positive button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
+ P.mPositiveButtonText = text;
+ P.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the negative button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the negative button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNegativeButton(@StringRes int textId, final OnClickListener listener) {
+ P.mNegativeButtonText = P.mContext.getText(textId);
+ P.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the negative button of the dialog is pressed.
+ * @param text The text to display in the negative button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNegativeButton(CharSequence text, final OnClickListener listener) {
+ P.mNegativeButtonText = text;
+ P.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the neutral button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the neutral button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNeutralButton(@StringRes int textId, final OnClickListener listener) {
+ P.mNeutralButtonText = P.mContext.getText(textId);
+ P.mNeutralButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the neutral button of the dialog is pressed.
+ * @param text The text to display in the neutral button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNeutralButton(CharSequence text, final OnClickListener listener) {
+ P.mNeutralButtonText = text;
+ P.mNeutralButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets whether the dialog is cancelable or not. Default is true.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setCancelable(boolean cancelable) {
+ P.mCancelable = cancelable;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called if the dialog is canceled.
+ *
+ * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
+ * being canceled or one of the supplied choices being selected.
+ * If you are interested in listening for all cases where the dialog is dismissed
+ * and not just when it is canceled, see
+ * {@link #setOnDismissListener(android.content.DialogInterface.OnDismissListener) setOnDismissListener}.</p>
+ * @see #setCancelable(boolean)
+ * @see #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setOnCancelListener(OnCancelListener onCancelListener) {
+ P.mOnCancelListener = onCancelListener;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called when the dialog is dismissed for any reason.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setOnDismissListener(OnDismissListener onDismissListener) {
+ P.mOnDismissListener = onDismissListener;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called if a key is dispatched to the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setOnKeyListener(OnKeyListener onKeyListener) {
+ P.mOnKeyListener = onKeyListener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener. This should be an array type i.e. R.array.foo
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setItems(@ArrayRes int itemsId, final OnClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setItems(CharSequence[] items, final OnClickListener listener) {
+ P.mItems = items;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items, which are supplied by the given {@link ListAdapter}, to be
+ * displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ *
+ * @param adapter The {@link ListAdapter} to supply the list of items
+ * @param listener The listener that will be called when an item is clicked.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setAdapter(final ListAdapter adapter, final OnClickListener listener) {
+ P.mAdapter = adapter;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items, which are supplied by the given {@link Cursor}, to be
+ * displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ *
+ * @param cursor The {@link Cursor} to supply the list of items
+ * @param listener The listener that will be called when an item is clicked.
+ * @param labelColumn The column name on the cursor containing the string to display
+ * in the label.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setCursor(final Cursor cursor, final OnClickListener listener,
+ String labelColumn) {
+ P.mCursor = cursor;
+ P.mLabelColumn = labelColumn;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * This should be an array type, e.g. R.array.foo. The list will have
+ * a check mark displayed to the right of the text for each checked
+ * item. Clicking on an item in the list will not dismiss the dialog.
+ * Clicking on a button will dismiss the dialog.
+ *
+ * @param itemsId the resource id of an array i.e. R.array.foo
+ * @param checkedItems specifies which items are checked. It should be null in which case no
+ * items are checked. If non null it must be exactly the same length as the array of
+ * items.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setMultiChoiceItems(@ArrayRes int itemsId, boolean[] checkedItems,
+ final OnMultiChoiceClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnCheckboxClickListener = listener;
+ P.mCheckedItems = checkedItems;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * The list will have a check mark displayed to the right of the text
+ * for each checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param items the text of the items to be displayed in the list.
+ * @param checkedItems specifies which items are checked. It should be null in which case no
+ * items are checked. If non null it must be exactly the same length as the array of
+ * items.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
+ final OnMultiChoiceClickListener listener) {
+ P.mItems = items;
+ P.mOnCheckboxClickListener = listener;
+ P.mCheckedItems = checkedItems;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * The list will have a check mark displayed to the right of the text
+ * for each checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param cursor the cursor used to provide the items.
+ * @param isCheckedColumn specifies the column name on the cursor to use to determine
+ * whether a checkbox is checked or not. It must return an integer value where 1
+ * means checked and 0 means unchecked.
+ * @param labelColumn The column name on the cursor containing the string to display in the
+ * label.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn,
+ final OnMultiChoiceClickListener listener) {
+ P.mCursor = cursor;
+ P.mOnCheckboxClickListener = listener;
+ P.mIsCheckedColumn = isCheckedColumn;
+ P.mLabelColumn = labelColumn;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. This should be an array type i.e.
+ * R.array.foo The list will have a check mark displayed to the right of the text for the
+ * checked item. Clicking on an item in the list will not dismiss the dialog. Clicking on a
+ * button will dismiss the dialog.
+ *
+ * @param itemsId the resource id of an array i.e. R.array.foo
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setSingleChoiceItems(@ArrayRes int itemsId, int checkedItem,
+ final OnClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param cursor the cursor to retrieve the items from.
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param labelColumn The column name on the cursor containing the string to display in the
+ * label.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn,
+ final OnClickListener listener) {
+ P.mCursor = cursor;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mLabelColumn = labelColumn;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param items the items to be displayed.
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, final OnClickListener listener) {
+ P.mItems = items;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param adapter The {@link ListAdapter} to supply the list of items
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setSingleChoiceItems(ListAdapter adapter, int checkedItem, final OnClickListener listener) {
+ P.mAdapter = adapter;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Sets a listener to be invoked when an item in the list is selected.
+ *
+ * @param listener the listener to be invoked
+ * @return this Builder object to allow for chaining of calls to set methods
+ * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
+ */
+ public Builder setOnItemSelectedListener(final AdapterView.OnItemSelectedListener listener) {
+ P.mOnItemSelectedListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a custom view resource to be the contents of the Dialog. The
+ * resource will be inflated, adding all top-level views to the screen.
+ *
+ * @param layoutResId Resource ID to be inflated.
+ * @return this Builder object to allow for chaining of calls to set
+ * methods
+ */
+ public Builder setView(int layoutResId) {
+ P.mView = null;
+ P.mViewLayoutResId = layoutResId;
+ P.mViewSpacingSpecified = false;
+ return this;
+ }
+
+ /**
+ * Sets a custom view to be the contents of the alert dialog.
+ * <p>
+ * When using a pre-Holo theme, if the supplied view is an instance of
+ * a {@link ListView} then the light background will be used.
+ * <p>
+ * <strong>Note:</strong> To ensure consistent styling, the custom view
+ * should be inflated or constructed using the alert dialog's themed
+ * context obtained via {@link #getContext()}.
+ *
+ * @param view the view to use as the contents of the alert dialog
+ * @return this Builder object to allow for chaining of calls to set
+ * methods
+ */
+ public Builder setView(View view) {
+ P.mView = view;
+ P.mViewLayoutResId = 0;
+ P.mViewSpacingSpecified = false;
+ return this;
+ }
+
+ /**
+ * Sets a custom view to be the contents of the alert dialog and
+ * specifies additional padding around that view.
+ * <p>
+ * When using a pre-Holo theme, if the supplied view is an instance of
+ * a {@link ListView} then the light background will be used.
+ * <p>
+ * <strong>Note:</strong> To ensure consistent styling, the custom view
+ * should be inflated or constructed using the alert dialog's themed
+ * context obtained via {@link #getContext()}.
+ *
+ * @param view the view to use as the contents of the alert dialog
+ * @param viewSpacingLeft spacing between the left edge of the view and
+ * the dialog frame
+ * @param viewSpacingTop spacing between the top edge of the view and
+ * the dialog frame
+ * @param viewSpacingRight spacing between the right edge of the view
+ * and the dialog frame
+ * @param viewSpacingBottom spacing between the bottom edge of the view
+ * and the dialog frame
+ * @return this Builder object to allow for chaining of calls to set
+ * methods
+ *
+ * @hide Remove once the framework usages have been replaced.
+ * @deprecated Set the padding on the view itself.
+ */
+ @Deprecated
+ public Builder setView(View view, int viewSpacingLeft, int viewSpacingTop,
+ int viewSpacingRight, int viewSpacingBottom) {
+ P.mView = view;
+ P.mViewLayoutResId = 0;
+ P.mViewSpacingSpecified = true;
+ P.mViewSpacingLeft = viewSpacingLeft;
+ P.mViewSpacingTop = viewSpacingTop;
+ P.mViewSpacingRight = viewSpacingRight;
+ P.mViewSpacingBottom = viewSpacingBottom;
+ return this;
+ }
+
+ /**
+ * Sets the alert dialog to use the inverse background, regardless of
+ * what the contents is.
+ *
+ * @param useInverseBackground whether to use the inverse background
+ * @return this Builder object to allow for chaining of calls to set methods
+ * @deprecated This flag is only used for pre-Material themes. Instead,
+ * specify the window background using on the alert dialog
+ * theme.
+ */
+ @Deprecated
+ public Builder setInverseBackgroundForced(boolean useInverseBackground) {
+ P.mForceInverseBackground = useInverseBackground;
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Builder setRecycleOnMeasureEnabled(boolean enabled) {
+ P.mRecycleOnMeasure = enabled;
+ return this;
+ }
+
+
+ /**
+ * Creates an {@link AlertDialog} with the arguments supplied to this
+ * builder.
+ * <p>
+ * Calling this method does not display the dialog. If no additional
+ * processing is needed, {@link #show()} may be called instead to both
+ * create and display the dialog.
+ */
+ public AlertDialog create() {
+ // Context has already been wrapped with the appropriate theme.
+ final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
+ P.apply(dialog.mAlert);
+ dialog.setCancelable(P.mCancelable);
+ if (P.mCancelable) {
+ dialog.setCanceledOnTouchOutside(true);
+ }
+ dialog.setOnCancelListener(P.mOnCancelListener);
+ dialog.setOnDismissListener(P.mOnDismissListener);
+ if (P.mOnKeyListener != null) {
+ dialog.setOnKeyListener(P.mOnKeyListener);
+ }
+ return dialog;
+ }
+
+ /**
+ * Creates an {@link AlertDialog} with the arguments supplied to this
+ * builder and immediately displays the dialog.
+ * <p>
+ * Calling this method is functionally identical to:
+ * <pre>
+ * AlertDialog dialog = builder.create();
+ * dialog.show();
+ * </pre>
+ */
+ public AlertDialog show() {
+ final AlertDialog dialog = create();
+ dialog.show();
+ return dialog;
+ }
+ }
+
+}
diff --git a/android/app/AliasActivity.java b/android/app/AliasActivity.java
new file mode 100644
index 00000000..37565298
--- /dev/null
+++ b/android/app/AliasActivity.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+
+/**
+ * Stub activity that launches another activity (and then finishes itself)
+ * based on information in its component's manifest meta-data. This is a
+ * simple way to implement an alias-like mechanism.
+ *
+ * To use this activity, you should include in the manifest for the associated
+ * component an entry named "android.app.alias". It is a reference to an XML
+ * resource describing an intent that launches the real application.
+ */
+public class AliasActivity extends Activity {
+ /**
+ * This is the name under which you should store in your component the
+ * meta-data information about the alias. It is a reference to an XML
+ * resource describing an intent that launches the real application.
+ * {@hide}
+ */
+ public final String ALIAS_META_DATA = "android.app.alias";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ XmlResourceParser parser = null;
+ try {
+ ActivityInfo ai = getPackageManager().getActivityInfo(
+ getComponentName(), PackageManager.GET_META_DATA);
+ parser = ai.loadXmlMetaData(getPackageManager(),
+ ALIAS_META_DATA);
+ if (parser == null) {
+ throw new RuntimeException("Alias requires a meta-data field "
+ + ALIAS_META_DATA);
+ }
+
+ Intent intent = parseAlias(parser);
+ if (intent == null) {
+ throw new RuntimeException(
+ "No <intent> tag found in alias description");
+ }
+
+ startActivity(intent);
+ finish();
+
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private Intent parseAlias(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ Intent intent = null;
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"alias".equals(nodeName)) {
+ throw new RuntimeException(
+ "Alias meta-data must start with <alias> tag; found"
+ + nodeName + " at " + parser.getPositionDescription());
+ }
+
+ int outerDepth = parser.getDepth();
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ nodeName = parser.getName();
+ if ("intent".equals(nodeName)) {
+ Intent gotIntent = Intent.parseIntent(getResources(), parser, attrs);
+ if (intent == null) intent = gotIntent;
+ } else {
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+
+ return intent;
+ }
+
+}
diff --git a/android/app/AppGlobals.java b/android/app/AppGlobals.java
new file mode 100644
index 00000000..2b6db8b5
--- /dev/null
+++ b/android/app/AppGlobals.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.pm.IPackageManager;
+
+/**
+ * Special private access for certain globals related to a process.
+ * @hide
+ */
+public class AppGlobals {
+ /**
+ * Return the first Application object made in the process.
+ * NOTE: Only works on the main thread.
+ */
+ public static Application getInitialApplication() {
+ return ActivityThread.currentApplication();
+ }
+
+ /**
+ * Return the package name of the first .apk loaded into the process.
+ * NOTE: Only works on the main thread.
+ */
+ public static String getInitialPackage() {
+ return ActivityThread.currentPackageName();
+ }
+
+ /**
+ * Return the raw interface to the package manager.
+ * @return The package manager.
+ */
+ public static IPackageManager getPackageManager() {
+ return ActivityThread.getPackageManager();
+ }
+
+ /**
+ * Gets the value of an integer core setting.
+ *
+ * @param key The setting key.
+ * @param defaultValue The setting default value.
+ * @return The core settings.
+ */
+ public static int getIntCoreSetting(String key, int defaultValue) {
+ ActivityThread currentActivityThread = ActivityThread.currentActivityThread();
+ if (currentActivityThread != null) {
+ return currentActivityThread.getIntCoreSetting(key, defaultValue);
+ } else {
+ return defaultValue;
+ }
+ }
+}
diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java
new file mode 100644
index 00000000..4bd85ae9
--- /dev/null
+++ b/android/app/AppOpsManager.java
@@ -0,0 +1,1969 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.media.AudioAttributes.AttributeUsage;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * API for interacting with "application operation" tracking.
+ *
+ * <p>This API is not generally intended for third party application developers; most
+ * features are only available to system applications.
+ */
+@SystemService(Context.APP_OPS_SERVICE)
+public class AppOpsManager {
+ /**
+ * <p>App ops allows callers to:</p>
+ *
+ * <ul>
+ * <li> Note when operations are happening, and find out if they are allowed for the current
+ * caller.</li>
+ * <li> Disallow specific apps from doing specific operations.</li>
+ * <li> Collect all of the current information about operations that have been executed or
+ * are not being allowed.</li>
+ * <li> Monitor for changes in whether an operation is allowed.</li>
+ * </ul>
+ *
+ * <p>Each operation is identified by a single integer; these integers are a fixed set of
+ * operations, enumerated by the OP_* constants.
+ *
+ * <p></p>When checking operations, the result is a "mode" integer indicating the current
+ * setting for the operation under that caller: MODE_ALLOWED, MODE_IGNORED (don't execute
+ * the operation but fake its behavior enough so that the caller doesn't crash),
+ * MODE_ERRORED (throw a SecurityException back to the caller; the normal operation calls
+ * will do this for you).
+ */
+
+ final Context mContext;
+ final IAppOpsService mService;
+ final ArrayMap<OnOpChangedListener, IAppOpsCallback> mModeWatchers
+ = new ArrayMap<OnOpChangedListener, IAppOpsCallback>();
+
+ static IBinder sToken;
+
+ /**
+ * Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: the given caller is
+ * allowed to perform the given operation.
+ */
+ public static final int MODE_ALLOWED = 0;
+
+ /**
+ * Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: the given caller is
+ * not allowed to perform the given operation, and this attempt should
+ * <em>silently fail</em> (it should not cause the app to crash).
+ */
+ public static final int MODE_IGNORED = 1;
+
+ /**
+ * Result from {@link #checkOpNoThrow}, {@link #noteOpNoThrow}, {@link #startOpNoThrow}: the
+ * given caller is not allowed to perform the given operation, and this attempt should
+ * cause it to have a fatal error, typically a {@link SecurityException}.
+ */
+ public static final int MODE_ERRORED = 2;
+
+ /**
+ * Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: the given caller should
+ * use its default security check. This mode is not normally used; it should only be used
+ * with appop permissions, and callers must explicitly check for it and deal with it.
+ */
+ public static final int MODE_DEFAULT = 3;
+
+ // when adding one of these:
+ // - increment _NUM_OP
+ // - add rows to sOpToSwitch, sOpToString, sOpNames, sOpToPerms, sOpDefault
+ // - add descriptive strings to Settings/res/values/arrays.xml
+ // - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
+
+ /** @hide No operation specified. */
+ public static final int OP_NONE = -1;
+ /** @hide Access to coarse location information. */
+ public static final int OP_COARSE_LOCATION = 0;
+ /** @hide Access to fine location information. */
+ public static final int OP_FINE_LOCATION = 1;
+ /** @hide Causing GPS to run. */
+ public static final int OP_GPS = 2;
+ /** @hide */
+ public static final int OP_VIBRATE = 3;
+ /** @hide */
+ public static final int OP_READ_CONTACTS = 4;
+ /** @hide */
+ public static final int OP_WRITE_CONTACTS = 5;
+ /** @hide */
+ public static final int OP_READ_CALL_LOG = 6;
+ /** @hide */
+ public static final int OP_WRITE_CALL_LOG = 7;
+ /** @hide */
+ public static final int OP_READ_CALENDAR = 8;
+ /** @hide */
+ public static final int OP_WRITE_CALENDAR = 9;
+ /** @hide */
+ public static final int OP_WIFI_SCAN = 10;
+ /** @hide */
+ public static final int OP_POST_NOTIFICATION = 11;
+ /** @hide */
+ public static final int OP_NEIGHBORING_CELLS = 12;
+ /** @hide */
+ public static final int OP_CALL_PHONE = 13;
+ /** @hide */
+ public static final int OP_READ_SMS = 14;
+ /** @hide */
+ public static final int OP_WRITE_SMS = 15;
+ /** @hide */
+ public static final int OP_RECEIVE_SMS = 16;
+ /** @hide */
+ public static final int OP_RECEIVE_EMERGECY_SMS = 17;
+ /** @hide */
+ public static final int OP_RECEIVE_MMS = 18;
+ /** @hide */
+ public static final int OP_RECEIVE_WAP_PUSH = 19;
+ /** @hide */
+ public static final int OP_SEND_SMS = 20;
+ /** @hide */
+ public static final int OP_READ_ICC_SMS = 21;
+ /** @hide */
+ public static final int OP_WRITE_ICC_SMS = 22;
+ /** @hide */
+ public static final int OP_WRITE_SETTINGS = 23;
+ /** @hide */
+ public static final int OP_SYSTEM_ALERT_WINDOW = 24;
+ /** @hide */
+ public static final int OP_ACCESS_NOTIFICATIONS = 25;
+ /** @hide */
+ public static final int OP_CAMERA = 26;
+ /** @hide */
+ public static final int OP_RECORD_AUDIO = 27;
+ /** @hide */
+ public static final int OP_PLAY_AUDIO = 28;
+ /** @hide */
+ public static final int OP_READ_CLIPBOARD = 29;
+ /** @hide */
+ public static final int OP_WRITE_CLIPBOARD = 30;
+ /** @hide */
+ public static final int OP_TAKE_MEDIA_BUTTONS = 31;
+ /** @hide */
+ public static final int OP_TAKE_AUDIO_FOCUS = 32;
+ /** @hide */
+ public static final int OP_AUDIO_MASTER_VOLUME = 33;
+ /** @hide */
+ public static final int OP_AUDIO_VOICE_VOLUME = 34;
+ /** @hide */
+ public static final int OP_AUDIO_RING_VOLUME = 35;
+ /** @hide */
+ public static final int OP_AUDIO_MEDIA_VOLUME = 36;
+ /** @hide */
+ public static final int OP_AUDIO_ALARM_VOLUME = 37;
+ /** @hide */
+ public static final int OP_AUDIO_NOTIFICATION_VOLUME = 38;
+ /** @hide */
+ public static final int OP_AUDIO_BLUETOOTH_VOLUME = 39;
+ /** @hide */
+ public static final int OP_WAKE_LOCK = 40;
+ /** @hide Continually monitoring location data. */
+ public static final int OP_MONITOR_LOCATION = 41;
+ /** @hide Continually monitoring location data with a relatively high power request. */
+ public static final int OP_MONITOR_HIGH_POWER_LOCATION = 42;
+ /** @hide Retrieve current usage stats via {@link UsageStatsManager}. */
+ public static final int OP_GET_USAGE_STATS = 43;
+ /** @hide */
+ public static final int OP_MUTE_MICROPHONE = 44;
+ /** @hide */
+ public static final int OP_TOAST_WINDOW = 45;
+ /** @hide Capture the device's display contents and/or audio */
+ public static final int OP_PROJECT_MEDIA = 46;
+ /** @hide Activate a VPN connection without user intervention. */
+ public static final int OP_ACTIVATE_VPN = 47;
+ /** @hide Access the WallpaperManagerAPI to write wallpapers. */
+ public static final int OP_WRITE_WALLPAPER = 48;
+ /** @hide Received the assist structure from an app. */
+ public static final int OP_ASSIST_STRUCTURE = 49;
+ /** @hide Received a screenshot from assist. */
+ public static final int OP_ASSIST_SCREENSHOT = 50;
+ /** @hide Read the phone state. */
+ public static final int OP_READ_PHONE_STATE = 51;
+ /** @hide Add voicemail messages to the voicemail content provider. */
+ public static final int OP_ADD_VOICEMAIL = 52;
+ /** @hide Access APIs for SIP calling over VOIP or WiFi. */
+ public static final int OP_USE_SIP = 53;
+ /** @hide Intercept outgoing calls. */
+ public static final int OP_PROCESS_OUTGOING_CALLS = 54;
+ /** @hide User the fingerprint API. */
+ public static final int OP_USE_FINGERPRINT = 55;
+ /** @hide Access to body sensors such as heart rate, etc. */
+ public static final int OP_BODY_SENSORS = 56;
+ /** @hide Read previously received cell broadcast messages. */
+ public static final int OP_READ_CELL_BROADCASTS = 57;
+ /** @hide Inject mock location into the system. */
+ public static final int OP_MOCK_LOCATION = 58;
+ /** @hide Read external storage. */
+ public static final int OP_READ_EXTERNAL_STORAGE = 59;
+ /** @hide Write external storage. */
+ public static final int OP_WRITE_EXTERNAL_STORAGE = 60;
+ /** @hide Turned on the screen. */
+ public static final int OP_TURN_SCREEN_ON = 61;
+ /** @hide Get device accounts. */
+ public static final int OP_GET_ACCOUNTS = 62;
+ /** @hide Control whether an application is allowed to run in the background. */
+ public static final int OP_RUN_IN_BACKGROUND = 63;
+ /** @hide */
+ public static final int OP_AUDIO_ACCESSIBILITY_VOLUME = 64;
+ /** @hide Read the phone number. */
+ public static final int OP_READ_PHONE_NUMBERS = 65;
+ /** @hide Request package installs through package installer */
+ public static final int OP_REQUEST_INSTALL_PACKAGES = 66;
+ /** @hide Enter picture-in-picture. */
+ public static final int OP_PICTURE_IN_PICTURE = 67;
+ /** @hide Instant app start foreground service. */
+ public static final int OP_INSTANT_APP_START_FOREGROUND = 68;
+ /** @hide Answer incoming phone calls */
+ public static final int OP_ANSWER_PHONE_CALLS = 69;
+ /** @hide Run jobs when in background */
+ public static final int OP_RUN_ANY_IN_BACKGROUND = 70;
+ /** @hide */
+ public static final int _NUM_OP = 71;
+
+ /** Access to coarse location information. */
+ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
+ /** Access to fine location information. */
+ public static final String OPSTR_FINE_LOCATION =
+ "android:fine_location";
+ /** Continually monitoring location data. */
+ public static final String OPSTR_MONITOR_LOCATION
+ = "android:monitor_location";
+ /** Continually monitoring location data with a relatively high power request. */
+ public static final String OPSTR_MONITOR_HIGH_POWER_LOCATION
+ = "android:monitor_location_high_power";
+ /** Access to {@link android.app.usage.UsageStatsManager}. */
+ public static final String OPSTR_GET_USAGE_STATS
+ = "android:get_usage_stats";
+ /** Activate a VPN connection without user intervention. @hide */
+ @SystemApi
+ public static final String OPSTR_ACTIVATE_VPN
+ = "android:activate_vpn";
+ /** Allows an application to read the user's contacts data. */
+ public static final String OPSTR_READ_CONTACTS
+ = "android:read_contacts";
+ /** Allows an application to write to the user's contacts data. */
+ public static final String OPSTR_WRITE_CONTACTS
+ = "android:write_contacts";
+ /** Allows an application to read the user's call log. */
+ public static final String OPSTR_READ_CALL_LOG
+ = "android:read_call_log";
+ /** Allows an application to write to the user's call log. */
+ public static final String OPSTR_WRITE_CALL_LOG
+ = "android:write_call_log";
+ /** Allows an application to read the user's calendar data. */
+ public static final String OPSTR_READ_CALENDAR
+ = "android:read_calendar";
+ /** Allows an application to write to the user's calendar data. */
+ public static final String OPSTR_WRITE_CALENDAR
+ = "android:write_calendar";
+ /** Allows an application to initiate a phone call. */
+ public static final String OPSTR_CALL_PHONE
+ = "android:call_phone";
+ /** Allows an application to read SMS messages. */
+ public static final String OPSTR_READ_SMS
+ = "android:read_sms";
+ /** Allows an application to receive SMS messages. */
+ public static final String OPSTR_RECEIVE_SMS
+ = "android:receive_sms";
+ /** Allows an application to receive MMS messages. */
+ public static final String OPSTR_RECEIVE_MMS
+ = "android:receive_mms";
+ /** Allows an application to receive WAP push messages. */
+ public static final String OPSTR_RECEIVE_WAP_PUSH
+ = "android:receive_wap_push";
+ /** Allows an application to send SMS messages. */
+ public static final String OPSTR_SEND_SMS
+ = "android:send_sms";
+ /** Required to be able to access the camera device. */
+ public static final String OPSTR_CAMERA
+ = "android:camera";
+ /** Required to be able to access the microphone device. */
+ public static final String OPSTR_RECORD_AUDIO
+ = "android:record_audio";
+ /** Required to access phone state related information. */
+ public static final String OPSTR_READ_PHONE_STATE
+ = "android:read_phone_state";
+ /** Required to access phone state related information. */
+ public static final String OPSTR_ADD_VOICEMAIL
+ = "android:add_voicemail";
+ /** 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 */
+ public static final String OPSTR_PROCESS_OUTGOING_CALLS
+ = "android:process_outgoing_calls";
+ /** Use the fingerprint API. */
+ public static final String OPSTR_USE_FINGERPRINT
+ = "android:use_fingerprint";
+ /** Access to body sensors such as heart rate, etc. */
+ public static final String OPSTR_BODY_SENSORS
+ = "android:body_sensors";
+ /** Read previously received cell broadcast messages. */
+ public static final String OPSTR_READ_CELL_BROADCASTS
+ = "android:read_cell_broadcasts";
+ /** Inject mock location into the system. */
+ public static final String OPSTR_MOCK_LOCATION
+ = "android:mock_location";
+ /** Read external storage. */
+ public static final String OPSTR_READ_EXTERNAL_STORAGE
+ = "android:read_external_storage";
+ /** Write external storage. */
+ public static final String OPSTR_WRITE_EXTERNAL_STORAGE
+ = "android:write_external_storage";
+ /** Required to draw on top of other apps. */
+ public static final String OPSTR_SYSTEM_ALERT_WINDOW
+ = "android:system_alert_window";
+ /** Required to write/modify/update system settingss. */
+ public static final String OPSTR_WRITE_SETTINGS
+ = "android:write_settings";
+ /** @hide Get device accounts. */
+ public static final String OPSTR_GET_ACCOUNTS
+ = "android:get_accounts";
+ public static final String OPSTR_READ_PHONE_NUMBERS
+ = "android:read_phone_numbers";
+ /** Access to picture-in-picture. */
+ public static final String OPSTR_PICTURE_IN_PICTURE
+ = "android:picture_in_picture";
+ /** @hide */
+ public static final String OPSTR_INSTANT_APP_START_FOREGROUND
+ = "android:instant_app_start_foreground";
+ /** Answer incoming phone calls */
+ public static final String OPSTR_ANSWER_PHONE_CALLS
+ = "android:answer_phone_calls";
+
+ // Warning: If an permission is added here it also has to be added to
+ // com.android.packageinstaller.permission.utils.EventLogger
+ private static final int[] RUNTIME_AND_APPOP_PERMISSIONS_OPS = {
+ // RUNTIME PERMISSIONS
+ // Contacts
+ OP_READ_CONTACTS,
+ OP_WRITE_CONTACTS,
+ OP_GET_ACCOUNTS,
+ // Calendar
+ OP_READ_CALENDAR,
+ OP_WRITE_CALENDAR,
+ // SMS
+ OP_SEND_SMS,
+ OP_RECEIVE_SMS,
+ OP_READ_SMS,
+ OP_RECEIVE_WAP_PUSH,
+ OP_RECEIVE_MMS,
+ OP_READ_CELL_BROADCASTS,
+ // Storage
+ OP_READ_EXTERNAL_STORAGE,
+ OP_WRITE_EXTERNAL_STORAGE,
+ // Location
+ OP_COARSE_LOCATION,
+ OP_FINE_LOCATION,
+ // Phone
+ OP_READ_PHONE_STATE,
+ OP_READ_PHONE_NUMBERS,
+ OP_CALL_PHONE,
+ OP_READ_CALL_LOG,
+ OP_WRITE_CALL_LOG,
+ OP_ADD_VOICEMAIL,
+ OP_USE_SIP,
+ OP_PROCESS_OUTGOING_CALLS,
+ OP_ANSWER_PHONE_CALLS,
+ // Microphone
+ OP_RECORD_AUDIO,
+ // Camera
+ OP_CAMERA,
+ // Body sensors
+ OP_BODY_SENSORS,
+
+ // APPOP PERMISSIONS
+ OP_ACCESS_NOTIFICATIONS,
+ OP_SYSTEM_ALERT_WINDOW,
+ OP_WRITE_SETTINGS,
+ OP_REQUEST_INSTALL_PACKAGES,
+ };
+
+ /**
+ * This maps each operation to the operation that serves as the
+ * switch to determine whether it is allowed. Generally this is
+ * a 1:1 mapping, but for some things (like location) that have
+ * multiple low-level operations being tracked that should be
+ * presented to the user as one switch then this can be used to
+ * make them all controlled by the same single operation.
+ */
+ private static int[] sOpToSwitch = new int[] {
+ OP_COARSE_LOCATION,
+ OP_COARSE_LOCATION,
+ OP_COARSE_LOCATION,
+ OP_VIBRATE,
+ OP_READ_CONTACTS,
+ OP_WRITE_CONTACTS,
+ OP_READ_CALL_LOG,
+ OP_WRITE_CALL_LOG,
+ OP_READ_CALENDAR,
+ OP_WRITE_CALENDAR,
+ OP_COARSE_LOCATION,
+ OP_POST_NOTIFICATION,
+ OP_COARSE_LOCATION,
+ OP_CALL_PHONE,
+ OP_READ_SMS,
+ OP_WRITE_SMS,
+ OP_RECEIVE_SMS,
+ OP_RECEIVE_SMS,
+ OP_RECEIVE_MMS,
+ OP_RECEIVE_WAP_PUSH,
+ OP_SEND_SMS,
+ OP_READ_SMS,
+ OP_WRITE_SMS,
+ OP_WRITE_SETTINGS,
+ OP_SYSTEM_ALERT_WINDOW,
+ OP_ACCESS_NOTIFICATIONS,
+ OP_CAMERA,
+ OP_RECORD_AUDIO,
+ OP_PLAY_AUDIO,
+ OP_READ_CLIPBOARD,
+ OP_WRITE_CLIPBOARD,
+ OP_TAKE_MEDIA_BUTTONS,
+ OP_TAKE_AUDIO_FOCUS,
+ OP_AUDIO_MASTER_VOLUME,
+ OP_AUDIO_VOICE_VOLUME,
+ OP_AUDIO_RING_VOLUME,
+ OP_AUDIO_MEDIA_VOLUME,
+ OP_AUDIO_ALARM_VOLUME,
+ OP_AUDIO_NOTIFICATION_VOLUME,
+ OP_AUDIO_BLUETOOTH_VOLUME,
+ OP_WAKE_LOCK,
+ OP_COARSE_LOCATION,
+ OP_COARSE_LOCATION,
+ OP_GET_USAGE_STATS,
+ OP_MUTE_MICROPHONE,
+ OP_TOAST_WINDOW,
+ OP_PROJECT_MEDIA,
+ OP_ACTIVATE_VPN,
+ OP_WRITE_WALLPAPER,
+ OP_ASSIST_STRUCTURE,
+ OP_ASSIST_SCREENSHOT,
+ OP_READ_PHONE_STATE,
+ OP_ADD_VOICEMAIL,
+ OP_USE_SIP,
+ OP_PROCESS_OUTGOING_CALLS,
+ OP_USE_FINGERPRINT,
+ OP_BODY_SENSORS,
+ OP_READ_CELL_BROADCASTS,
+ OP_MOCK_LOCATION,
+ OP_READ_EXTERNAL_STORAGE,
+ OP_WRITE_EXTERNAL_STORAGE,
+ OP_TURN_SCREEN_ON,
+ OP_GET_ACCOUNTS,
+ OP_RUN_IN_BACKGROUND,
+ OP_AUDIO_ACCESSIBILITY_VOLUME,
+ OP_READ_PHONE_NUMBERS,
+ OP_REQUEST_INSTALL_PACKAGES,
+ OP_PICTURE_IN_PICTURE,
+ OP_INSTANT_APP_START_FOREGROUND,
+ OP_ANSWER_PHONE_CALLS,
+ OP_RUN_ANY_IN_BACKGROUND,
+ };
+
+ /**
+ * This maps each operation to the public string constant for it.
+ * If it doesn't have a public string constant, it maps to null.
+ */
+ private static String[] sOpToString = new String[] {
+ OPSTR_COARSE_LOCATION,
+ OPSTR_FINE_LOCATION,
+ null,
+ null,
+ OPSTR_READ_CONTACTS,
+ OPSTR_WRITE_CONTACTS,
+ OPSTR_READ_CALL_LOG,
+ OPSTR_WRITE_CALL_LOG,
+ OPSTR_READ_CALENDAR,
+ OPSTR_WRITE_CALENDAR,
+ null,
+ null,
+ null,
+ OPSTR_CALL_PHONE,
+ OPSTR_READ_SMS,
+ null,
+ OPSTR_RECEIVE_SMS,
+ null,
+ OPSTR_RECEIVE_MMS,
+ OPSTR_RECEIVE_WAP_PUSH,
+ OPSTR_SEND_SMS,
+ null,
+ null,
+ OPSTR_WRITE_SETTINGS,
+ OPSTR_SYSTEM_ALERT_WINDOW,
+ null,
+ OPSTR_CAMERA,
+ OPSTR_RECORD_AUDIO,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ OPSTR_MONITOR_LOCATION,
+ OPSTR_MONITOR_HIGH_POWER_LOCATION,
+ OPSTR_GET_USAGE_STATS,
+ null,
+ null,
+ null,
+ OPSTR_ACTIVATE_VPN,
+ null,
+ null,
+ null,
+ OPSTR_READ_PHONE_STATE,
+ OPSTR_ADD_VOICEMAIL,
+ OPSTR_USE_SIP,
+ OPSTR_PROCESS_OUTGOING_CALLS,
+ OPSTR_USE_FINGERPRINT,
+ OPSTR_BODY_SENSORS,
+ OPSTR_READ_CELL_BROADCASTS,
+ OPSTR_MOCK_LOCATION,
+ OPSTR_READ_EXTERNAL_STORAGE,
+ OPSTR_WRITE_EXTERNAL_STORAGE,
+ null,
+ OPSTR_GET_ACCOUNTS,
+ null,
+ null, // OP_AUDIO_ACCESSIBILITY_VOLUME
+ OPSTR_READ_PHONE_NUMBERS,
+ null, // OP_REQUEST_INSTALL_PACKAGES
+ OPSTR_PICTURE_IN_PICTURE,
+ OPSTR_INSTANT_APP_START_FOREGROUND,
+ OPSTR_ANSWER_PHONE_CALLS,
+ null, // OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * This provides a simple name for each operation to be used
+ * in debug output.
+ */
+ private static String[] sOpNames = new String[] {
+ "COARSE_LOCATION",
+ "FINE_LOCATION",
+ "GPS",
+ "VIBRATE",
+ "READ_CONTACTS",
+ "WRITE_CONTACTS",
+ "READ_CALL_LOG",
+ "WRITE_CALL_LOG",
+ "READ_CALENDAR",
+ "WRITE_CALENDAR",
+ "WIFI_SCAN",
+ "POST_NOTIFICATION",
+ "NEIGHBORING_CELLS",
+ "CALL_PHONE",
+ "READ_SMS",
+ "WRITE_SMS",
+ "RECEIVE_SMS",
+ "RECEIVE_EMERGECY_SMS",
+ "RECEIVE_MMS",
+ "RECEIVE_WAP_PUSH",
+ "SEND_SMS",
+ "READ_ICC_SMS",
+ "WRITE_ICC_SMS",
+ "WRITE_SETTINGS",
+ "SYSTEM_ALERT_WINDOW",
+ "ACCESS_NOTIFICATIONS",
+ "CAMERA",
+ "RECORD_AUDIO",
+ "PLAY_AUDIO",
+ "READ_CLIPBOARD",
+ "WRITE_CLIPBOARD",
+ "TAKE_MEDIA_BUTTONS",
+ "TAKE_AUDIO_FOCUS",
+ "AUDIO_MASTER_VOLUME",
+ "AUDIO_VOICE_VOLUME",
+ "AUDIO_RING_VOLUME",
+ "AUDIO_MEDIA_VOLUME",
+ "AUDIO_ALARM_VOLUME",
+ "AUDIO_NOTIFICATION_VOLUME",
+ "AUDIO_BLUETOOTH_VOLUME",
+ "WAKE_LOCK",
+ "MONITOR_LOCATION",
+ "MONITOR_HIGH_POWER_LOCATION",
+ "GET_USAGE_STATS",
+ "MUTE_MICROPHONE",
+ "TOAST_WINDOW",
+ "PROJECT_MEDIA",
+ "ACTIVATE_VPN",
+ "WRITE_WALLPAPER",
+ "ASSIST_STRUCTURE",
+ "ASSIST_SCREENSHOT",
+ "OP_READ_PHONE_STATE",
+ "ADD_VOICEMAIL",
+ "USE_SIP",
+ "PROCESS_OUTGOING_CALLS",
+ "USE_FINGERPRINT",
+ "BODY_SENSORS",
+ "READ_CELL_BROADCASTS",
+ "MOCK_LOCATION",
+ "READ_EXTERNAL_STORAGE",
+ "WRITE_EXTERNAL_STORAGE",
+ "TURN_ON_SCREEN",
+ "GET_ACCOUNTS",
+ "RUN_IN_BACKGROUND",
+ "AUDIO_ACCESSIBILITY_VOLUME",
+ "READ_PHONE_NUMBERS",
+ "REQUEST_INSTALL_PACKAGES",
+ "PICTURE_IN_PICTURE",
+ "INSTANT_APP_START_FOREGROUND",
+ "ANSWER_PHONE_CALLS",
+ "RUN_ANY_IN_BACKGROUND",
+ };
+
+ /**
+ * This optionally maps a permission to an operation. If there
+ * is no permission associated with an operation, it is null.
+ */
+ private static String[] sOpPerms = new String[] {
+ android.Manifest.permission.ACCESS_COARSE_LOCATION,
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ null,
+ android.Manifest.permission.VIBRATE,
+ android.Manifest.permission.READ_CONTACTS,
+ android.Manifest.permission.WRITE_CONTACTS,
+ android.Manifest.permission.READ_CALL_LOG,
+ android.Manifest.permission.WRITE_CALL_LOG,
+ android.Manifest.permission.READ_CALENDAR,
+ android.Manifest.permission.WRITE_CALENDAR,
+ android.Manifest.permission.ACCESS_WIFI_STATE,
+ null, // no permission required for notifications
+ null, // neighboring cells shares the coarse location perm
+ android.Manifest.permission.CALL_PHONE,
+ android.Manifest.permission.READ_SMS,
+ null, // no permission required for writing sms
+ android.Manifest.permission.RECEIVE_SMS,
+ android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST,
+ android.Manifest.permission.RECEIVE_MMS,
+ android.Manifest.permission.RECEIVE_WAP_PUSH,
+ android.Manifest.permission.SEND_SMS,
+ android.Manifest.permission.READ_SMS,
+ null, // no permission required for writing icc sms
+ android.Manifest.permission.WRITE_SETTINGS,
+ android.Manifest.permission.SYSTEM_ALERT_WINDOW,
+ android.Manifest.permission.ACCESS_NOTIFICATIONS,
+ android.Manifest.permission.CAMERA,
+ android.Manifest.permission.RECORD_AUDIO,
+ null, // no permission for playing audio
+ null, // no permission for reading clipboard
+ null, // no permission for writing clipboard
+ null, // no permission for taking media buttons
+ null, // no permission for taking audio focus
+ null, // no permission for changing master volume
+ null, // no permission for changing voice volume
+ null, // no permission for changing ring volume
+ null, // no permission for changing media volume
+ null, // no permission for changing alarm volume
+ null, // no permission for changing notification volume
+ null, // no permission for changing bluetooth volume
+ android.Manifest.permission.WAKE_LOCK,
+ null, // no permission for generic location monitoring
+ null, // no permission for high power location monitoring
+ android.Manifest.permission.PACKAGE_USAGE_STATS,
+ null, // no permission for muting/unmuting microphone
+ null, // no permission for displaying toasts
+ null, // no permission for projecting media
+ null, // no permission for activating vpn
+ null, // no permission for supporting wallpaper
+ null, // no permission for receiving assist structure
+ null, // no permission for receiving assist screenshot
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.ADD_VOICEMAIL,
+ Manifest.permission.USE_SIP,
+ Manifest.permission.PROCESS_OUTGOING_CALLS,
+ Manifest.permission.USE_FINGERPRINT,
+ Manifest.permission.BODY_SENSORS,
+ Manifest.permission.READ_CELL_BROADCASTS,
+ null,
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ null, // no permission for turning the screen on
+ Manifest.permission.GET_ACCOUNTS,
+ null, // no permission for running in background
+ null, // no permission for changing accessibility volume
+ Manifest.permission.READ_PHONE_NUMBERS,
+ Manifest.permission.REQUEST_INSTALL_PACKAGES,
+ null, // no permission for entering picture-in-picture on hide
+ Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE,
+ Manifest.permission.ANSWER_PHONE_CALLS,
+ null, // no permission for OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * Specifies whether an Op should be restricted by a user restriction.
+ * Each Op should be filled with a restriction string from UserManager or
+ * null to specify it is not affected by any user restriction.
+ */
+ private static String[] sOpRestrictions = new String[] {
+ UserManager.DISALLOW_SHARE_LOCATION, //COARSE_LOCATION
+ UserManager.DISALLOW_SHARE_LOCATION, //FINE_LOCATION
+ UserManager.DISALLOW_SHARE_LOCATION, //GPS
+ null, //VIBRATE
+ null, //READ_CONTACTS
+ null, //WRITE_CONTACTS
+ UserManager.DISALLOW_OUTGOING_CALLS, //READ_CALL_LOG
+ UserManager.DISALLOW_OUTGOING_CALLS, //WRITE_CALL_LOG
+ null, //READ_CALENDAR
+ null, //WRITE_CALENDAR
+ UserManager.DISALLOW_SHARE_LOCATION, //WIFI_SCAN
+ null, //POST_NOTIFICATION
+ null, //NEIGHBORING_CELLS
+ null, //CALL_PHONE
+ UserManager.DISALLOW_SMS, //READ_SMS
+ UserManager.DISALLOW_SMS, //WRITE_SMS
+ UserManager.DISALLOW_SMS, //RECEIVE_SMS
+ null, //RECEIVE_EMERGENCY_SMS
+ UserManager.DISALLOW_SMS, //RECEIVE_MMS
+ null, //RECEIVE_WAP_PUSH
+ UserManager.DISALLOW_SMS, //SEND_SMS
+ UserManager.DISALLOW_SMS, //READ_ICC_SMS
+ UserManager.DISALLOW_SMS, //WRITE_ICC_SMS
+ null, //WRITE_SETTINGS
+ UserManager.DISALLOW_CREATE_WINDOWS, //SYSTEM_ALERT_WINDOW
+ null, //ACCESS_NOTIFICATIONS
+ UserManager.DISALLOW_CAMERA, //CAMERA
+ UserManager.DISALLOW_RECORD_AUDIO, //RECORD_AUDIO
+ null, //PLAY_AUDIO
+ null, //READ_CLIPBOARD
+ null, //WRITE_CLIPBOARD
+ null, //TAKE_MEDIA_BUTTONS
+ null, //TAKE_AUDIO_FOCUS
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MASTER_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_VOICE_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_RING_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MEDIA_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ALARM_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_NOTIFICATION_VOLUME
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_BLUETOOTH_VOLUME
+ null, //WAKE_LOCK
+ UserManager.DISALLOW_SHARE_LOCATION, //MONITOR_LOCATION
+ UserManager.DISALLOW_SHARE_LOCATION, //MONITOR_HIGH_POWER_LOCATION
+ null, //GET_USAGE_STATS
+ UserManager.DISALLOW_UNMUTE_MICROPHONE, // MUTE_MICROPHONE
+ UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW
+ null, //PROJECT_MEDIA
+ null, // ACTIVATE_VPN
+ UserManager.DISALLOW_WALLPAPER, // WRITE_WALLPAPER
+ null, // ASSIST_STRUCTURE
+ null, // ASSIST_SCREENSHOT
+ null, // READ_PHONE_STATE
+ null, // ADD_VOICEMAIL
+ null, // USE_SIP
+ null, // PROCESS_OUTGOING_CALLS
+ null, // USE_FINGERPRINT
+ null, // BODY_SENSORS
+ null, // READ_CELL_BROADCASTS
+ null, // MOCK_LOCATION
+ null, // READ_EXTERNAL_STORAGE
+ null, // WRITE_EXTERNAL_STORAGE
+ null, // TURN_ON_SCREEN
+ null, // GET_ACCOUNTS
+ null, // RUN_IN_BACKGROUND
+ UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ACCESSIBILITY_VOLUME
+ null, // READ_PHONE_NUMBERS
+ null, // REQUEST_INSTALL_PACKAGES
+ null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
+ null, // INSTANT_APP_START_FOREGROUND
+ null, // ANSWER_PHONE_CALLS
+ null, // OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * This specifies whether each option should allow the system
+ * (and system ui) to bypass the user restriction when active.
+ */
+ private static boolean[] sOpAllowSystemRestrictionBypass = new boolean[] {
+ true, //COARSE_LOCATION
+ true, //FINE_LOCATION
+ false, //GPS
+ false, //VIBRATE
+ false, //READ_CONTACTS
+ false, //WRITE_CONTACTS
+ false, //READ_CALL_LOG
+ false, //WRITE_CALL_LOG
+ false, //READ_CALENDAR
+ false, //WRITE_CALENDAR
+ true, //WIFI_SCAN
+ false, //POST_NOTIFICATION
+ false, //NEIGHBORING_CELLS
+ false, //CALL_PHONE
+ false, //READ_SMS
+ false, //WRITE_SMS
+ false, //RECEIVE_SMS
+ false, //RECEIVE_EMERGECY_SMS
+ false, //RECEIVE_MMS
+ false, //RECEIVE_WAP_PUSH
+ false, //SEND_SMS
+ false, //READ_ICC_SMS
+ false, //WRITE_ICC_SMS
+ false, //WRITE_SETTINGS
+ true, //SYSTEM_ALERT_WINDOW
+ false, //ACCESS_NOTIFICATIONS
+ false, //CAMERA
+ false, //RECORD_AUDIO
+ false, //PLAY_AUDIO
+ false, //READ_CLIPBOARD
+ false, //WRITE_CLIPBOARD
+ false, //TAKE_MEDIA_BUTTONS
+ false, //TAKE_AUDIO_FOCUS
+ false, //AUDIO_MASTER_VOLUME
+ false, //AUDIO_VOICE_VOLUME
+ false, //AUDIO_RING_VOLUME
+ false, //AUDIO_MEDIA_VOLUME
+ false, //AUDIO_ALARM_VOLUME
+ false, //AUDIO_NOTIFICATION_VOLUME
+ false, //AUDIO_BLUETOOTH_VOLUME
+ false, //WAKE_LOCK
+ false, //MONITOR_LOCATION
+ false, //MONITOR_HIGH_POWER_LOCATION
+ false, //GET_USAGE_STATS
+ false, //MUTE_MICROPHONE
+ true, //TOAST_WINDOW
+ false, //PROJECT_MEDIA
+ false, //ACTIVATE_VPN
+ false, //WALLPAPER
+ false, //ASSIST_STRUCTURE
+ false, //ASSIST_SCREENSHOT
+ false, //READ_PHONE_STATE
+ false, //ADD_VOICEMAIL
+ false, // USE_SIP
+ false, // PROCESS_OUTGOING_CALLS
+ false, // USE_FINGERPRINT
+ false, // BODY_SENSORS
+ false, // READ_CELL_BROADCASTS
+ false, // MOCK_LOCATION
+ false, // READ_EXTERNAL_STORAGE
+ false, // WRITE_EXTERNAL_STORAGE
+ false, // TURN_ON_SCREEN
+ false, // GET_ACCOUNTS
+ false, // RUN_IN_BACKGROUND
+ false, // AUDIO_ACCESSIBILITY_VOLUME
+ false, // READ_PHONE_NUMBERS
+ false, // REQUEST_INSTALL_PACKAGES
+ false, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
+ false, // INSTANT_APP_START_FOREGROUND
+ false, // ANSWER_PHONE_CALLS
+ false, // OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * This specifies the default mode for each operation.
+ */
+ private static int[] sOpDefaultMode = new int[] {
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_IGNORED, // OP_WRITE_SMS
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_DEFAULT, // OP_WRITE_SETTINGS
+ AppOpsManager.MODE_DEFAULT, // OP_SYSTEM_ALERT_WINDOW
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_DEFAULT, // OP_GET_USAGE_STATS
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA
+ AppOpsManager.MODE_IGNORED, // OP_ACTIVATE_VPN
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ERRORED, // OP_MOCK_LOCATION
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED, // OP_TURN_ON_SCREEN
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_ALLOWED, // OP_RUN_IN_BACKGROUND
+ AppOpsManager.MODE_ALLOWED, // OP_AUDIO_ACCESSIBILITY_VOLUME
+ AppOpsManager.MODE_ALLOWED,
+ AppOpsManager.MODE_DEFAULT, // OP_REQUEST_INSTALL_PACKAGES
+ AppOpsManager.MODE_ALLOWED, // OP_PICTURE_IN_PICTURE
+ AppOpsManager.MODE_DEFAULT, // OP_INSTANT_APP_START_FOREGROUND
+ AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS
+ AppOpsManager.MODE_ALLOWED, // OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * This specifies whether each option is allowed to be reset
+ * when resetting all app preferences. Disable reset for
+ * app ops that are under strong control of some part of the
+ * system (such as OP_WRITE_SMS, which should be allowed only
+ * for whichever app is selected as the current SMS app).
+ */
+ private static boolean[] sOpDisableReset = new boolean[] {
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true, // OP_WRITE_SMS
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false, // OP_AUDIO_ACCESSIBILITY_VOLUME
+ false,
+ false, // OP_REQUEST_INSTALL_PACKAGES
+ false, // OP_PICTURE_IN_PICTURE
+ false,
+ false, // ANSWER_PHONE_CALLS
+ false, // OP_RUN_ANY_IN_BACKGROUND
+ };
+
+ /**
+ * Mapping from an app op name to the app op code.
+ */
+ private static HashMap<String, Integer> sOpStrToOp = new HashMap<>();
+
+ /**
+ * Mapping from a permission to the corresponding app op.
+ */
+ private static HashMap<String, Integer> sPermToOp = new HashMap<>();
+
+ static {
+ if (sOpToSwitch.length != _NUM_OP) {
+ throw new IllegalStateException("sOpToSwitch length " + sOpToSwitch.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpToString.length != _NUM_OP) {
+ throw new IllegalStateException("sOpToString length " + sOpToString.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpNames.length != _NUM_OP) {
+ throw new IllegalStateException("sOpNames length " + sOpNames.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpPerms.length != _NUM_OP) {
+ throw new IllegalStateException("sOpPerms length " + sOpPerms.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpDefaultMode.length != _NUM_OP) {
+ throw new IllegalStateException("sOpDefaultMode length " + sOpDefaultMode.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpDisableReset.length != _NUM_OP) {
+ throw new IllegalStateException("sOpDisableReset length " + sOpDisableReset.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpRestrictions.length != _NUM_OP) {
+ throw new IllegalStateException("sOpRestrictions length " + sOpRestrictions.length
+ + " should be " + _NUM_OP);
+ }
+ if (sOpAllowSystemRestrictionBypass.length != _NUM_OP) {
+ throw new IllegalStateException("sOpAllowSYstemRestrictionsBypass length "
+ + sOpRestrictions.length + " should be " + _NUM_OP);
+ }
+ for (int i=0; i<_NUM_OP; i++) {
+ if (sOpToString[i] != null) {
+ sOpStrToOp.put(sOpToString[i], i);
+ }
+ }
+ for (int op : RUNTIME_AND_APPOP_PERMISSIONS_OPS) {
+ if (sOpPerms[op] != null) {
+ sPermToOp.put(sOpPerms[op], op);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the op switch that controls the given operation.
+ * @hide
+ */
+ public static int opToSwitch(int op) {
+ return sOpToSwitch[op];
+ }
+
+ /**
+ * Retrieve a non-localized name for the operation, for debugging output.
+ * @hide
+ */
+ public static String opToName(int op) {
+ if (op == OP_NONE) return "NONE";
+ return op < sOpNames.length ? sOpNames[op] : ("Unknown(" + op + ")");
+ }
+
+ /**
+ * @hide
+ */
+ public static int strDebugOpToOp(String op) {
+ for (int i=0; i<sOpNames.length; i++) {
+ if (sOpNames[i].equals(op)) {
+ return i;
+ }
+ }
+ throw new IllegalArgumentException("Unknown operation string: " + op);
+ }
+
+ /**
+ * Retrieve the permission associated with an operation, or null if there is not one.
+ * @hide
+ */
+ public static String opToPermission(int op) {
+ return sOpPerms[op];
+ }
+
+ /**
+ * Retrieve the user restriction associated with an operation, or null if there is not one.
+ * @hide
+ */
+ public static String opToRestriction(int op) {
+ return sOpRestrictions[op];
+ }
+
+ /**
+ * Retrieve the app op code for a permission, or null if there is not one.
+ * This API is intended to be used for mapping runtime or appop permissions
+ * to the corresponding app op.
+ * @hide
+ */
+ public static int permissionToOpCode(String permission) {
+ Integer boxedOpCode = sPermToOp.get(permission);
+ return boxedOpCode != null ? boxedOpCode : OP_NONE;
+ }
+
+ /**
+ * Retrieve whether the op allows the system (and system ui) to
+ * bypass the user restriction.
+ * @hide
+ */
+ public static boolean opAllowSystemBypassRestriction(int op) {
+ return sOpAllowSystemRestrictionBypass[op];
+ }
+
+ /**
+ * Retrieve the default mode for the operation.
+ * @hide
+ */
+ public static int opToDefaultMode(int op) {
+ return sOpDefaultMode[op];
+ }
+
+ /**
+ * Retrieve whether the op allows itself to be reset.
+ * @hide
+ */
+ public static boolean opAllowsReset(int op) {
+ return !sOpDisableReset[op];
+ }
+
+ /**
+ * Class holding all of the operation information associated with an app.
+ * @hide
+ */
+ public static class PackageOps implements Parcelable {
+ private final String mPackageName;
+ private final int mUid;
+ private final List<OpEntry> mEntries;
+
+ public PackageOps(String packageName, int uid, List<OpEntry> entries) {
+ mPackageName = packageName;
+ mUid = uid;
+ mEntries = entries;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public List<OpEntry> getOps() {
+ return mEntries;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPackageName);
+ dest.writeInt(mUid);
+ dest.writeInt(mEntries.size());
+ for (int i=0; i<mEntries.size(); i++) {
+ mEntries.get(i).writeToParcel(dest, flags);
+ }
+ }
+
+ PackageOps(Parcel source) {
+ mPackageName = source.readString();
+ mUid = source.readInt();
+ mEntries = new ArrayList<OpEntry>();
+ final int N = source.readInt();
+ for (int i=0; i<N; i++) {
+ mEntries.add(OpEntry.CREATOR.createFromParcel(source));
+ }
+ }
+
+ public static final Creator<PackageOps> CREATOR = new Creator<PackageOps>() {
+ @Override public PackageOps createFromParcel(Parcel source) {
+ return new PackageOps(source);
+ }
+
+ @Override public PackageOps[] newArray(int size) {
+ return new PackageOps[size];
+ }
+ };
+ }
+
+ /**
+ * Class holding the information about one unique operation of an application.
+ * @hide
+ */
+ public static class OpEntry implements Parcelable {
+ private final int mOp;
+ private final int mMode;
+ private final long mTime;
+ private final long mRejectTime;
+ private final int mDuration;
+ private final int mProxyUid;
+ private final String mProxyPackageName;
+
+ public OpEntry(int op, int mode, long time, long rejectTime, int duration,
+ int proxyUid, String proxyPackage) {
+ mOp = op;
+ mMode = mode;
+ mTime = time;
+ mRejectTime = rejectTime;
+ mDuration = duration;
+ mProxyUid = proxyUid;
+ mProxyPackageName = proxyPackage;
+ }
+
+ public int getOp() {
+ return mOp;
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ public long getTime() {
+ return mTime;
+ }
+
+ public long getRejectTime() {
+ return mRejectTime;
+ }
+
+ public boolean isRunning() {
+ return mDuration == -1;
+ }
+
+ public int getDuration() {
+ return mDuration == -1 ? (int)(System.currentTimeMillis()-mTime) : mDuration;
+ }
+
+ public int getProxyUid() {
+ return mProxyUid;
+ }
+
+ public String getProxyPackageName() {
+ return mProxyPackageName;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mOp);
+ dest.writeInt(mMode);
+ dest.writeLong(mTime);
+ dest.writeLong(mRejectTime);
+ dest.writeInt(mDuration);
+ dest.writeInt(mProxyUid);
+ dest.writeString(mProxyPackageName);
+ }
+
+ OpEntry(Parcel source) {
+ mOp = source.readInt();
+ mMode = source.readInt();
+ mTime = source.readLong();
+ mRejectTime = source.readLong();
+ mDuration = source.readInt();
+ mProxyUid = source.readInt();
+ mProxyPackageName = source.readString();
+ }
+
+ public static final Creator<OpEntry> CREATOR = new Creator<OpEntry>() {
+ @Override public OpEntry createFromParcel(Parcel source) {
+ return new OpEntry(source);
+ }
+
+ @Override public OpEntry[] newArray(int size) {
+ return new OpEntry[size];
+ }
+ };
+ }
+
+ /**
+ * Callback for notification of changes to operation state.
+ */
+ public interface OnOpChangedListener {
+ public void onOpChanged(String op, String packageName);
+ }
+
+ /**
+ * Callback for notification of changes to operation state.
+ * This allows you to see the raw op codes instead of strings.
+ * @hide
+ */
+ public static class OnOpChangedInternalListener implements OnOpChangedListener {
+ public void onOpChanged(String op, String packageName) { }
+ public void onOpChanged(int op, String packageName) { }
+ }
+
+ AppOpsManager(Context context, IAppOpsService service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Retrieve current operation state for all applications.
+ *
+ * @param ops The set of operations you are interested in, or null if you want all of them.
+ * @hide
+ */
+ public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) {
+ try {
+ return mService.getPackagesForOps(ops);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve current operation state for one application.
+ *
+ * @param uid The uid of the application of interest.
+ * @param packageName The name of the application of interest.
+ * @param ops The set of operations you are interested in, or null if you want all of them.
+ * @hide
+ */
+ public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) {
+ try {
+ return mService.getOpsForPackage(uid, packageName, ops);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets given app op in the specified mode for app ops in the UID.
+ * This applies to all apps currently in the UID or installed in
+ * this UID in the future.
+ *
+ * @param code The app op.
+ * @param uid The UID for which to set the app.
+ * @param mode The app op mode to set.
+ * @hide
+ */
+ public void setUidMode(int code, int uid, int mode) {
+ try {
+ mService.setUidMode(code, uid, mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets given app op in the specified mode for app ops in the UID.
+ * This applies to all apps currently in the UID or installed in
+ * this UID in the future.
+ *
+ * @param appOp The app op.
+ * @param uid The UID for which to set the app.
+ * @param mode The app op mode to set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS)
+ public void setUidMode(String appOp, int uid, int mode) {
+ try {
+ mService.setUidMode(AppOpsManager.strOpToOp(appOp), uid, mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setUserRestriction(int code, boolean restricted, IBinder token) {
+ setUserRestriction(code, restricted, token, /*exceptionPackages*/null);
+ }
+
+ /** @hide */
+ public void setUserRestriction(int code, boolean restricted, IBinder token,
+ String[] exceptionPackages) {
+ setUserRestrictionForUser(code, restricted, token, exceptionPackages, mContext.getUserId());
+ }
+
+ /** @hide */
+ public void setUserRestrictionForUser(int code, boolean restricted, IBinder token,
+ String[] exceptionPackages, int userId) {
+ try {
+ mService.setUserRestriction(code, restricted, token, userId, exceptionPackages);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setMode(int code, int uid, String packageName, int mode) {
+ try {
+ mService.setMode(code, uid, packageName, mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set a non-persisted restriction on an audio operation at a stream-level.
+ * Restrictions are temporary additional constraints imposed on top of the persisted rules
+ * defined by {@link #setMode}.
+ *
+ * @param code The operation to restrict.
+ * @param usage The {@link android.media.AudioAttributes} usage value.
+ * @param mode The restriction mode (MODE_IGNORED,MODE_ERRORED) or MODE_ALLOWED to unrestrict.
+ * @param exceptionPackages Optional list of packages to exclude from the restriction.
+ * @hide
+ */
+ public void setRestriction(int code, @AttributeUsage int usage, int mode,
+ String[] exceptionPackages) {
+ try {
+ final int uid = Binder.getCallingUid();
+ mService.setAudioRestriction(code, usage, uid, mode, exceptionPackages);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void resetAllModes() {
+ try {
+ mService.resetAllModes(UserHandle.myUserId(), null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the app op name associated with a given permission.
+ * The app op name is one of the public constants defined
+ * in this class such as {@link #OPSTR_COARSE_LOCATION}.
+ * This API is intended to be used for mapping runtime
+ * permissions to the corresponding app op.
+ *
+ * @param permission The permission.
+ * @return The app op associated with the permission or null.
+ */
+ public static String permissionToOp(String permission) {
+ final Integer opCode = sPermToOp.get(permission);
+ if (opCode == null) {
+ return null;
+ }
+ return sOpToString[opCode];
+ }
+
+ /**
+ * Monitor for changes to the operating mode for the given op in the given app package.
+ * @param op The operation to monitor, one of OPSTR_*.
+ * @param packageName The name of the application to monitor.
+ * @param callback Where to report changes.
+ */
+ public void startWatchingMode(String op, String packageName,
+ final OnOpChangedListener callback) {
+ startWatchingMode(strOpToOp(op), packageName, callback);
+ }
+
+ /**
+ * Monitor for changes to the operating mode for the given op in the given app package.
+ * @param op The operation to monitor, one of OP_*.
+ * @param packageName The name of the application to monitor.
+ * @param callback Where to report changes.
+ * @hide
+ */
+ public void startWatchingMode(int op, String packageName, final OnOpChangedListener callback) {
+ synchronized (mModeWatchers) {
+ IAppOpsCallback cb = mModeWatchers.get(callback);
+ if (cb == null) {
+ cb = new IAppOpsCallback.Stub() {
+ public void opChanged(int op, int uid, String packageName) {
+ if (callback instanceof OnOpChangedInternalListener) {
+ ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName);
+ }
+ if (sOpToString[op] != null) {
+ callback.onOpChanged(sOpToString[op], packageName);
+ }
+ }
+ };
+ mModeWatchers.put(callback, cb);
+ }
+ try {
+ mService.startWatchingMode(op, packageName, cb);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Stop monitoring that was previously started with {@link #startWatchingMode}. All
+ * monitoring associated with this callback will be removed.
+ */
+ public void stopWatchingMode(OnOpChangedListener callback) {
+ synchronized (mModeWatchers) {
+ IAppOpsCallback cb = mModeWatchers.get(callback);
+ if (cb != null) {
+ try {
+ mService.stopWatchingMode(cb);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ private String buildSecurityExceptionMsg(int op, int uid, String packageName) {
+ return packageName + " from uid " + uid + " not allowed to perform " + sOpNames[op];
+ }
+
+ /**
+ * {@hide}
+ */
+ public static int strOpToOp(String op) {
+ Integer val = sOpStrToOp.get(op);
+ if (val == null) {
+ throw new IllegalArgumentException("Unknown operation string: " + op);
+ }
+ return val;
+ }
+
+ /**
+ * Do a quick check for whether an application might be able to perform an operation.
+ * This is <em>not</em> a security check; you must use {@link #noteOp(String, int, String)}
+ * or {@link #startOp(String, int, String)} for your actual security checks, which also
+ * ensure that the given uid and package name are consistent. This function can just be
+ * used for a quick check to see if an operation has been disabled for the application,
+ * as an early reject of some work. This does not modify the time stamp or other data
+ * about the operation.
+ * @param op The operation to check. One of the OPSTR_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ */
+ public int checkOp(String op, int uid, String packageName) {
+ return checkOp(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Like {@link #checkOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ */
+ public int checkOpNoThrow(String op, int uid, String packageName) {
+ return checkOpNoThrow(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Make note of an application performing an operation. Note that you must pass
+ * in both the uid and name of the application to be checked; this function will verify
+ * that these two match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for this app will be updated to
+ * the current time.
+ * @param op The operation to note. One of the OPSTR_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ */
+ public int noteOp(String op, int uid, String packageName) {
+ return noteOp(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Like {@link #noteOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ */
+ public int noteOpNoThrow(String op, int uid, String packageName) {
+ return noteOpNoThrow(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Make note of an application performing an operation on behalf of another
+ * application when handling an IPC. Note that you must pass the package name
+ * of the application that is being proxied while its UID will be inferred from
+ * the IPC state; this function will verify that the calling uid and proxied
+ * package name match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for the proxied app and
+ * your app will be updated to the current time.
+ * @param op The operation to note. One of the OPSTR_* constants.
+ * @param proxiedPackageName The name of the application calling into the proxy application.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ */
+ public int noteProxyOp(String op, String proxiedPackageName) {
+ return noteProxyOp(strOpToOp(op), proxiedPackageName);
+ }
+
+ /**
+ * Like {@link #noteProxyOp(String, String)} but instead
+ * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}.
+ */
+ public int noteProxyOpNoThrow(String op, String proxiedPackageName) {
+ return noteProxyOpNoThrow(strOpToOp(op), proxiedPackageName);
+ }
+
+ /**
+ * Report that an application has started executing a long-running operation. Note that you
+ * must pass in both the uid and name of the application to be checked; this function will
+ * verify that these two match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for this app will be updated to
+ * the current time and the operation will be marked as "running". In this case you must
+ * later call {@link #finishOp(String, int, String)} to report when the application is no
+ * longer performing the operation.
+ * @param op The operation to start. One of the OPSTR_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ */
+ public int startOp(String op, int uid, String packageName) {
+ return startOp(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Like {@link #startOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ */
+ public int startOpNoThrow(String op, int uid, String packageName) {
+ return startOpNoThrow(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Report that an application is no longer performing an operation that had previously
+ * been started with {@link #startOp(String, int, String)}. There is no validation of input
+ * or result; the parameters supplied here must be the exact same ones previously passed
+ * in when starting the operation.
+ */
+ public void finishOp(String op, int uid, String packageName) {
+ finishOp(strOpToOp(op), uid, packageName);
+ }
+
+ /**
+ * Do a quick check for whether an application might be able to perform an operation.
+ * This is <em>not</em> a security check; you must use {@link #noteOp(int, int, String)}
+ * or {@link #startOp(int, int, String)} for your actual security checks, which also
+ * ensure that the given uid and package name are consistent. This function can just be
+ * used for a quick check to see if an operation has been disabled for the application,
+ * as an early reject of some work. This does not modify the time stamp or other data
+ * about the operation.
+ * @param op The operation to check. One of the OP_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ * @hide
+ */
+ public int checkOp(int op, int uid, String packageName) {
+ try {
+ int mode = mService.checkOperation(op, uid, packageName);
+ if (mode == MODE_ERRORED) {
+ throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
+ }
+ return mode;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #checkOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ * @hide
+ */
+ public int checkOpNoThrow(int op, int uid, String packageName) {
+ try {
+ return mService.checkOperation(op, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Do a quick check to validate if a package name belongs to a UID.
+ *
+ * @throws SecurityException if the package name doesn't belong to the given
+ * UID, or if ownership cannot be verified.
+ */
+ public void checkPackage(int uid, String packageName) {
+ try {
+ if (mService.checkPackage(uid, packageName) != MODE_ALLOWED) {
+ throw new SecurityException(
+ "Package " + packageName + " does not belong to " + uid);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #checkOp} but at a stream-level for audio operations.
+ * @hide
+ */
+ public int checkAudioOp(int op, int stream, int uid, String packageName) {
+ try {
+ final int mode = mService.checkAudioOperation(op, stream, uid, packageName);
+ if (mode == MODE_ERRORED) {
+ throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
+ }
+ return mode;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #checkAudioOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ * @hide
+ */
+ public int checkAudioOpNoThrow(int op, int stream, int uid, String packageName) {
+ try {
+ return mService.checkAudioOperation(op, stream, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Make note of an application performing an operation. Note that you must pass
+ * in both the uid and name of the application to be checked; this function will verify
+ * that these two match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for this app will be updated to
+ * the current time.
+ * @param op The operation to note. One of the OP_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ * @hide
+ */
+ public int noteOp(int op, int uid, String packageName) {
+ try {
+ int mode = mService.noteOperation(op, uid, packageName);
+ if (mode == MODE_ERRORED) {
+ throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
+ }
+ return mode;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Make note of an application performing an operation on behalf of another
+ * application when handling an IPC. Note that you must pass the package name
+ * of the application that is being proxied while its UID will be inferred from
+ * the IPC state; this function will verify that the calling uid and proxied
+ * package name match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for the proxied app and
+ * your app will be updated to the current time.
+ * @param op The operation to note. One of the OPSTR_* constants.
+ * @param proxiedPackageName The name of the application calling into the proxy application.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the proxy or proxied app has been configured to
+ * crash on this op.
+ *
+ * @hide
+ */
+ public int noteProxyOp(int op, String proxiedPackageName) {
+ int mode = noteProxyOpNoThrow(op, proxiedPackageName);
+ if (mode == MODE_ERRORED) {
+ throw new SecurityException("Proxy package " + mContext.getOpPackageName()
+ + " from uid " + Process.myUid() + " or calling package "
+ + proxiedPackageName + " from uid " + Binder.getCallingUid()
+ + " not allowed to perform " + sOpNames[op]);
+ }
+ return mode;
+ }
+
+ /**
+ * Like {@link #noteProxyOp(int, String)} but instead
+ * of throwing a {@link SecurityException} it returns {@link #MODE_ERRORED}.
+ * @hide
+ */
+ public int noteProxyOpNoThrow(int op, String proxiedPackageName) {
+ try {
+ return mService.noteProxyOperation(op, mContext.getOpPackageName(),
+ Binder.getCallingUid(), proxiedPackageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #noteOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ * @hide
+ */
+ public int noteOpNoThrow(int op, int uid, String packageName) {
+ try {
+ return mService.noteOperation(op, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public int noteOp(int op) {
+ return noteOp(op, Process.myUid(), mContext.getOpPackageName());
+ }
+
+ /** @hide */
+ public static IBinder getToken(IAppOpsService service) {
+ synchronized (AppOpsManager.class) {
+ if (sToken != null) {
+ return sToken;
+ }
+ try {
+ sToken = service.getToken(new Binder());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return sToken;
+ }
+ }
+
+ /**
+ * Report that an application has started executing a long-running operation. Note that you
+ * must pass in both the uid and name of the application to be checked; this function will
+ * verify that these two match, and if not, return {@link #MODE_IGNORED}. If this call
+ * succeeds, the last execution time of the operation for this app will be updated to
+ * the current time and the operation will be marked as "running". In this case you must
+ * later call {@link #finishOp(int, int, String)} to report when the application is no
+ * longer performing the operation.
+ * @param op The operation to start. One of the OP_* constants.
+ * @param uid The user id of the application attempting to perform the operation.
+ * @param packageName The name of the application attempting to perform the operation.
+ * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
+ * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
+ * causing the app to crash).
+ * @throws SecurityException If the app has been configured to crash on this op.
+ * @hide
+ */
+ public int startOp(int op, int uid, String packageName) {
+ try {
+ int mode = mService.startOperation(getToken(mService), op, uid, packageName);
+ if (mode == MODE_ERRORED) {
+ throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
+ }
+ return mode;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #startOp} but instead of throwing a {@link SecurityException} it
+ * returns {@link #MODE_ERRORED}.
+ * @hide
+ */
+ public int startOpNoThrow(int op, int uid, String packageName) {
+ try {
+ return mService.startOperation(getToken(mService), op, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public int startOp(int op) {
+ return startOp(op, Process.myUid(), mContext.getOpPackageName());
+ }
+
+ /**
+ * Report that an application is no longer performing an operation that had previously
+ * been started with {@link #startOp(int, int, String)}. There is no validation of input
+ * or result; the parameters supplied here must be the exact same ones previously passed
+ * in when starting the operation.
+ * @hide
+ */
+ public void finishOp(int op, int uid, String packageName) {
+ try {
+ mService.finishOperation(getToken(mService), op, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void finishOp(int op) {
+ finishOp(op, Process.myUid(), mContext.getOpPackageName());
+ }
+
+ /** @hide */
+ public boolean isOperationActive(int code, int uid, String packageName) {
+ try {
+ return mService.isOperationActive(code, uid, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/app/Application.java b/android/app/Application.java
new file mode 100644
index 00000000..156df36a
--- /dev/null
+++ b/android/app/Application.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import java.util.ArrayList;
+
+import android.annotation.CallSuper;
+import android.content.ComponentCallbacks;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+
+/**
+ * Base class for maintaining global application state. You can provide your own
+ * implementation by creating a subclass and specifying the fully-qualified name
+ * of this subclass as the <code>"android:name"</code> attribute in your
+ * AndroidManifest.xml's <code>&lt;application&gt;</code> tag. The Application
+ * class, or your subclass of the Application class, is instantiated before any
+ * other class when the process for your application/package is created.
+ *
+ * <p class="note"><strong>Note: </strong>There is normally no need to subclass
+ * Application. In most situations, static singletons can provide the same
+ * functionality in a more modular way. If your singleton needs a global
+ * context (for example to register broadcast receivers), include
+ * {@link android.content.Context#getApplicationContext() Context.getApplicationContext()}
+ * as a {@link android.content.Context} argument when invoking your singleton's
+ * <code>getInstance()</code> method.
+ * </p>
+ */
+public class Application extends ContextWrapper implements ComponentCallbacks2 {
+ private ArrayList<ComponentCallbacks> mComponentCallbacks =
+ new ArrayList<ComponentCallbacks>();
+ private ArrayList<ActivityLifecycleCallbacks> mActivityLifecycleCallbacks =
+ new ArrayList<ActivityLifecycleCallbacks>();
+ private ArrayList<OnProvideAssistDataListener> mAssistCallbacks = null;
+
+ /** @hide */
+ public LoadedApk mLoadedApk;
+
+ public interface ActivityLifecycleCallbacks {
+ void onActivityCreated(Activity activity, Bundle savedInstanceState);
+ void onActivityStarted(Activity activity);
+ void onActivityResumed(Activity activity);
+ void onActivityPaused(Activity activity);
+ void onActivityStopped(Activity activity);
+ void onActivitySaveInstanceState(Activity activity, Bundle outState);
+ void onActivityDestroyed(Activity activity);
+ }
+
+ /**
+ * Callback interface for use with {@link Application#registerOnProvideAssistDataListener}
+ * and {@link Application#unregisterOnProvideAssistDataListener}.
+ */
+ public interface OnProvideAssistDataListener {
+ /**
+ * This is called when the user is requesting an assist, to build a full
+ * {@link Intent#ACTION_ASSIST} Intent with all of the context of the current
+ * application. You can override this method to place into the bundle anything
+ * you would like to appear in the {@link Intent#EXTRA_ASSIST_CONTEXT} part
+ * of the assist Intent.
+ */
+ public void onProvideAssistData(Activity activity, Bundle data);
+ }
+
+ public Application() {
+ super(null);
+ }
+
+ /**
+ * Called when the application is starting, before any activity, service,
+ * or receiver objects (excluding content providers) have been created.
+ * Implementations should be as quick as possible (for example using
+ * lazy initialization of state) since the time spent in this function
+ * directly impacts the performance of starting the first activity,
+ * service, or receiver in a process.
+ * If you override this method, be sure to call super.onCreate().
+ */
+ @CallSuper
+ public void onCreate() {
+ }
+
+ /**
+ * This method is for use in emulated process environments. It will
+ * never be called on a production Android device, where processes are
+ * removed by simply killing them; no user code (including this callback)
+ * is executed when doing so.
+ */
+ @CallSuper
+ public void onTerminate() {
+ }
+
+ @CallSuper
+ public void onConfigurationChanged(Configuration newConfig) {
+ Object[] callbacks = collectComponentCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ComponentCallbacks)callbacks[i]).onConfigurationChanged(newConfig);
+ }
+ }
+ }
+
+ @CallSuper
+ public void onLowMemory() {
+ Object[] callbacks = collectComponentCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ComponentCallbacks)callbacks[i]).onLowMemory();
+ }
+ }
+ }
+
+ @CallSuper
+ public void onTrimMemory(int level) {
+ Object[] callbacks = collectComponentCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ Object c = callbacks[i];
+ if (c instanceof ComponentCallbacks2) {
+ ((ComponentCallbacks2)c).onTrimMemory(level);
+ }
+ }
+ }
+ }
+
+ public void registerComponentCallbacks(ComponentCallbacks callback) {
+ synchronized (mComponentCallbacks) {
+ mComponentCallbacks.add(callback);
+ }
+ }
+
+ public void unregisterComponentCallbacks(ComponentCallbacks callback) {
+ synchronized (mComponentCallbacks) {
+ mComponentCallbacks.remove(callback);
+ }
+ }
+
+ public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
+ synchronized (mActivityLifecycleCallbacks) {
+ mActivityLifecycleCallbacks.add(callback);
+ }
+ }
+
+ public void unregisterActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
+ synchronized (mActivityLifecycleCallbacks) {
+ mActivityLifecycleCallbacks.remove(callback);
+ }
+ }
+
+ public void registerOnProvideAssistDataListener(OnProvideAssistDataListener callback) {
+ synchronized (this) {
+ if (mAssistCallbacks == null) {
+ mAssistCallbacks = new ArrayList<OnProvideAssistDataListener>();
+ }
+ mAssistCallbacks.add(callback);
+ }
+ }
+
+ public void unregisterOnProvideAssistDataListener(OnProvideAssistDataListener callback) {
+ synchronized (this) {
+ if (mAssistCallbacks != null) {
+ mAssistCallbacks.remove(callback);
+ }
+ }
+ }
+
+ // ------------------ Internal API ------------------
+
+ /**
+ * @hide
+ */
+ /* package */ final void attach(Context context) {
+ attachBaseContext(context);
+ mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
+ }
+
+ /* package */ void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
+ savedInstanceState);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivityStarted(Activity activity) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityStarted(activity);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivityResumed(Activity activity) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityResumed(activity);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivityPaused(Activity activity) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityPaused(activity);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivityStopped(Activity activity) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityStopped(activity);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivitySaveInstanceState(Activity activity, Bundle outState) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivitySaveInstanceState(activity,
+ outState);
+ }
+ }
+ }
+
+ /* package */ void dispatchActivityDestroyed(Activity activity) {
+ Object[] callbacks = collectActivityLifecycleCallbacks();
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((ActivityLifecycleCallbacks)callbacks[i]).onActivityDestroyed(activity);
+ }
+ }
+ }
+
+ private Object[] collectComponentCallbacks() {
+ Object[] callbacks = null;
+ synchronized (mComponentCallbacks) {
+ if (mComponentCallbacks.size() > 0) {
+ callbacks = mComponentCallbacks.toArray();
+ }
+ }
+ return callbacks;
+ }
+
+ private Object[] collectActivityLifecycleCallbacks() {
+ Object[] callbacks = null;
+ synchronized (mActivityLifecycleCallbacks) {
+ if (mActivityLifecycleCallbacks.size() > 0) {
+ callbacks = mActivityLifecycleCallbacks.toArray();
+ }
+ }
+ return callbacks;
+ }
+
+ /* package */ void dispatchOnProvideAssistData(Activity activity, Bundle data) {
+ Object[] callbacks;
+ synchronized (this) {
+ if (mAssistCallbacks == null) {
+ return;
+ }
+ callbacks = mAssistCallbacks.toArray();
+ }
+ if (callbacks != null) {
+ for (int i=0; i<callbacks.length; i++) {
+ ((OnProvideAssistDataListener)callbacks[i]).onProvideAssistData(activity, data);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/android/app/ApplicationErrorReport.java b/android/app/ApplicationErrorReport.java
new file mode 100644
index 00000000..e6452619
--- /dev/null
+++ b/android/app/ApplicationErrorReport.java
@@ -0,0 +1,701 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Printer;
+import android.util.Slog;
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Describes an application error.
+ *
+ * A report has a type, which is one of
+ * <ul>
+ * <li> {@link #TYPE_NONE} uninitialized instance of {@link ApplicationErrorReport}.
+ * <li> {@link #TYPE_CRASH} application crash. Information about the crash
+ * is stored in {@link #crashInfo}.
+ * <li> {@link #TYPE_ANR} application not responding. Information about the
+ * ANR is stored in {@link #anrInfo}.
+ * <li> {@link #TYPE_BATTERY} user reported application is using too much
+ * battery. Information about the battery use is stored in {@link #batteryInfo}.
+ * <li> {@link #TYPE_RUNNING_SERVICE} user reported application is leaving an
+ * unneeded serive running. Information about the battery use is stored in
+ * {@link #runningServiceInfo}.
+ * </ul>
+ */
+
+public class ApplicationErrorReport implements Parcelable {
+ // System property defining error report receiver for system apps
+ static final String SYSTEM_APPS_ERROR_RECEIVER_PROPERTY = "ro.error.receiver.system.apps";
+
+ // System property defining default error report receiver
+ static final String DEFAULT_ERROR_RECEIVER_PROPERTY = "ro.error.receiver.default";
+
+ /**
+ * Uninitialized error report.
+ */
+ public static final int TYPE_NONE = 0;
+
+ /**
+ * An error report about an application crash.
+ */
+ public static final int TYPE_CRASH = 1;
+
+ /**
+ * An error report about an application that's not responding.
+ */
+ public static final int TYPE_ANR = 2;
+
+ /**
+ * An error report about an application that's consuming too much battery.
+ */
+ public static final int TYPE_BATTERY = 3;
+
+ /**
+ * A report from a user to a developer about a running service that the
+ * user doesn't think should be running.
+ */
+ public static final int TYPE_RUNNING_SERVICE = 5;
+
+ /**
+ * Type of this report. Can be one of {@link #TYPE_NONE},
+ * {@link #TYPE_CRASH}, {@link #TYPE_ANR}, {@link #TYPE_BATTERY},
+ * or {@link #TYPE_RUNNING_SERVICE}.
+ */
+ public int type;
+
+ /**
+ * Package name of the application.
+ */
+ public String packageName;
+
+ /**
+ * Package name of the application which installed the application this
+ * report pertains to.
+ * This identifies which market the application came from.
+ */
+ public String installerPackageName;
+
+ /**
+ * Process name of the application.
+ */
+ public String processName;
+
+ /**
+ * Time at which the error occurred.
+ */
+ public long time;
+
+ /**
+ * Set if the app is on the system image.
+ */
+ public boolean systemApp;
+
+ /**
+ * If this report is of type {@link #TYPE_CRASH}, contains an instance
+ * of CrashInfo describing the crash; otherwise null.
+ */
+ public CrashInfo crashInfo;
+
+ /**
+ * If this report is of type {@link #TYPE_ANR}, contains an instance
+ * of AnrInfo describing the ANR; otherwise null.
+ */
+ public AnrInfo anrInfo;
+
+ /**
+ * If this report is of type {@link #TYPE_BATTERY}, contains an instance
+ * of BatteryInfo; otherwise null.
+ */
+ public BatteryInfo batteryInfo;
+
+ /**
+ * If this report is of type {@link #TYPE_RUNNING_SERVICE}, contains an instance
+ * of RunningServiceInfo; otherwise null.
+ */
+ public RunningServiceInfo runningServiceInfo;
+
+ /**
+ * Create an uninitialized instance of {@link ApplicationErrorReport}.
+ */
+ public ApplicationErrorReport() {
+ }
+
+ /**
+ * Create an instance of {@link ApplicationErrorReport} initialized from
+ * a parcel.
+ */
+ ApplicationErrorReport(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public static ComponentName getErrorReportReceiver(Context context,
+ String packageName, int appFlags) {
+ // check if error reporting is enabled in secure settings
+ int enabled = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.SEND_ACTION_APP_ERROR, 0);
+ if (enabled == 0) {
+ return null;
+ }
+
+ PackageManager pm = context.getPackageManager();
+
+ // look for receiver in the installer package
+ String candidate = null;
+ ComponentName result = null;
+
+ try {
+ candidate = pm.getInstallerPackageName(packageName);
+ } catch (IllegalArgumentException e) {
+ // the package could already removed
+ }
+
+ if (candidate != null) {
+ result = getErrorReportReceiver(pm, packageName, candidate);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ // if the error app is on the system image, look for system apps
+ // error receiver
+ if ((appFlags&ApplicationInfo.FLAG_SYSTEM) != 0) {
+ candidate = SystemProperties.get(SYSTEM_APPS_ERROR_RECEIVER_PROPERTY);
+ result = getErrorReportReceiver(pm, packageName, candidate);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ // if there is a default receiver, try that
+ candidate = SystemProperties.get(DEFAULT_ERROR_RECEIVER_PROPERTY);
+ return getErrorReportReceiver(pm, packageName, candidate);
+ }
+
+ /**
+ * Return activity in receiverPackage that handles ACTION_APP_ERROR.
+ *
+ * @param pm PackageManager instance
+ * @param errorPackage package which caused the error
+ * @param receiverPackage candidate package to receive the error
+ * @return activity component within receiverPackage which handles
+ * ACTION_APP_ERROR, or null if not found
+ */
+ static ComponentName getErrorReportReceiver(PackageManager pm, String errorPackage,
+ String receiverPackage) {
+ if (receiverPackage == null || receiverPackage.length() == 0) {
+ return null;
+ }
+
+ // break the loop if it's the error report receiver package that crashed
+ if (receiverPackage.equals(errorPackage)) {
+ return null;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_APP_ERROR);
+ intent.setPackage(receiverPackage);
+ ResolveInfo info = pm.resolveActivity(intent, 0);
+ if (info == null || info.activityInfo == null) {
+ return null;
+ }
+ return new ComponentName(receiverPackage, info.activityInfo.name);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(type);
+ dest.writeString(packageName);
+ dest.writeString(installerPackageName);
+ dest.writeString(processName);
+ dest.writeLong(time);
+ dest.writeInt(systemApp ? 1 : 0);
+ dest.writeInt(crashInfo != null ? 1 : 0);
+
+ switch (type) {
+ case TYPE_CRASH:
+ if (crashInfo != null) {
+ crashInfo.writeToParcel(dest, flags);
+ }
+ break;
+ case TYPE_ANR:
+ anrInfo.writeToParcel(dest, flags);
+ break;
+ case TYPE_BATTERY:
+ batteryInfo.writeToParcel(dest, flags);
+ break;
+ case TYPE_RUNNING_SERVICE:
+ runningServiceInfo.writeToParcel(dest, flags);
+ break;
+ }
+ }
+
+ public void readFromParcel(Parcel in) {
+ type = in.readInt();
+ packageName = in.readString();
+ installerPackageName = in.readString();
+ processName = in.readString();
+ time = in.readLong();
+ systemApp = in.readInt() == 1;
+ boolean hasCrashInfo = in.readInt() == 1;
+
+ switch (type) {
+ case TYPE_CRASH:
+ crashInfo = hasCrashInfo ? new CrashInfo(in) : null;
+ anrInfo = null;
+ batteryInfo = null;
+ runningServiceInfo = null;
+ break;
+ case TYPE_ANR:
+ anrInfo = new AnrInfo(in);
+ crashInfo = null;
+ batteryInfo = null;
+ runningServiceInfo = null;
+ break;
+ case TYPE_BATTERY:
+ batteryInfo = new BatteryInfo(in);
+ anrInfo = null;
+ crashInfo = null;
+ runningServiceInfo = null;
+ break;
+ case TYPE_RUNNING_SERVICE:
+ batteryInfo = null;
+ anrInfo = null;
+ crashInfo = null;
+ runningServiceInfo = new RunningServiceInfo(in);
+ break;
+ }
+ }
+
+ /**
+ * Describes an application crash.
+ */
+ public static class CrashInfo {
+ /**
+ * Class name of the exception that caused the crash.
+ */
+ public String exceptionClassName;
+
+ /**
+ * Message stored in the exception.
+ */
+ public String exceptionMessage;
+
+ /**
+ * File which the exception was thrown from.
+ */
+ public String throwFileName;
+
+ /**
+ * Class which the exception was thrown from.
+ */
+ public String throwClassName;
+
+ /**
+ * Method which the exception was thrown from.
+ */
+ public String throwMethodName;
+
+ /**
+ * Line number the exception was thrown from.
+ */
+ public int throwLineNumber;
+
+ /**
+ * Stack trace.
+ */
+ public String stackTrace;
+
+ /**
+ * Create an uninitialized instance of CrashInfo.
+ */
+ public CrashInfo() {
+ }
+
+ /**
+ * Create an instance of CrashInfo initialized from an exception.
+ */
+ public CrashInfo(Throwable tr) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new FastPrintWriter(sw, false, 256);
+ tr.printStackTrace(pw);
+ pw.flush();
+ stackTrace = sanitizeString(sw.toString());
+ exceptionMessage = tr.getMessage();
+
+ // Populate fields with the "root cause" exception
+ Throwable rootTr = tr;
+ while (tr.getCause() != null) {
+ tr = tr.getCause();
+ if (tr.getStackTrace() != null && tr.getStackTrace().length > 0) {
+ rootTr = tr;
+ }
+ String msg = tr.getMessage();
+ if (msg != null && msg.length() > 0) {
+ exceptionMessage = msg;
+ }
+ }
+
+ exceptionClassName = rootTr.getClass().getName();
+ if (rootTr.getStackTrace().length > 0) {
+ StackTraceElement trace = rootTr.getStackTrace()[0];
+ throwFileName = trace.getFileName();
+ throwClassName = trace.getClassName();
+ throwMethodName = trace.getMethodName();
+ throwLineNumber = trace.getLineNumber();
+ } else {
+ throwFileName = "unknown";
+ throwClassName = "unknown";
+ throwMethodName = "unknown";
+ throwLineNumber = 0;
+ }
+
+ exceptionMessage = sanitizeString(exceptionMessage);
+ }
+
+ /** {@hide} */
+ public void appendStackTrace(String tr) {
+ stackTrace = sanitizeString(stackTrace + tr);
+ }
+
+ /**
+ * Ensure that the string is of reasonable size, truncating from the middle if needed.
+ */
+ private String sanitizeString(String s) {
+ int prefixLength = 10 * 1024;
+ int suffixLength = 10 * 1024;
+ int acceptableLength = prefixLength + suffixLength;
+
+ if (s != null && s.length() > acceptableLength) {
+ String replacement =
+ "\n[TRUNCATED " + (s.length() - acceptableLength) + " CHARS]\n";
+
+ StringBuilder sb = new StringBuilder(acceptableLength + replacement.length());
+ sb.append(s.substring(0, prefixLength));
+ sb.append(replacement);
+ sb.append(s.substring(s.length() - suffixLength));
+ return sb.toString();
+ }
+ return s;
+ }
+
+ /**
+ * Create an instance of CrashInfo initialized from a Parcel.
+ */
+ public CrashInfo(Parcel in) {
+ exceptionClassName = in.readString();
+ exceptionMessage = in.readString();
+ throwFileName = in.readString();
+ throwClassName = in.readString();
+ throwMethodName = in.readString();
+ throwLineNumber = in.readInt();
+ stackTrace = in.readString();
+ }
+
+ /**
+ * Save a CrashInfo instance to a parcel.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ int start = dest.dataPosition();
+ dest.writeString(exceptionClassName);
+ dest.writeString(exceptionMessage);
+ dest.writeString(throwFileName);
+ dest.writeString(throwClassName);
+ dest.writeString(throwMethodName);
+ dest.writeInt(throwLineNumber);
+ dest.writeString(stackTrace);
+ int total = dest.dataPosition()-start;
+ if (Binder.CHECK_PARCEL_SIZE && total > 20*1024) {
+ Slog.d("Error", "ERR: exClass=" + exceptionClassName);
+ Slog.d("Error", "ERR: exMsg=" + exceptionMessage);
+ Slog.d("Error", "ERR: file=" + throwFileName);
+ Slog.d("Error", "ERR: class=" + throwClassName);
+ Slog.d("Error", "ERR: method=" + throwMethodName + " line=" + throwLineNumber);
+ Slog.d("Error", "ERR: stack=" + stackTrace);
+ Slog.d("Error", "ERR: TOTAL BYTES WRITTEN: " + (dest.dataPosition()-start));
+ }
+ }
+
+ /**
+ * Dump a CrashInfo instance to a Printer.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "exceptionClassName: " + exceptionClassName);
+ pw.println(prefix + "exceptionMessage: " + exceptionMessage);
+ pw.println(prefix + "throwFileName: " + throwFileName);
+ pw.println(prefix + "throwClassName: " + throwClassName);
+ pw.println(prefix + "throwMethodName: " + throwMethodName);
+ pw.println(prefix + "throwLineNumber: " + throwLineNumber);
+ pw.println(prefix + "stackTrace: " + stackTrace);
+ }
+ }
+
+ /**
+ * Parcelable version of {@link CrashInfo}
+ *
+ * @hide
+ */
+ public static class ParcelableCrashInfo extends CrashInfo implements Parcelable {
+ /**
+ * Create an uninitialized instance of CrashInfo.
+ */
+ public ParcelableCrashInfo() {
+ }
+
+ /**
+ * Create an instance of CrashInfo initialized from an exception.
+ */
+ public ParcelableCrashInfo(Throwable tr) {
+ super(tr);
+ }
+
+ public ParcelableCrashInfo(Parcel in) {
+ super(in);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<ParcelableCrashInfo> CREATOR =
+ new Parcelable.Creator<ParcelableCrashInfo>() {
+ @Override
+ public ParcelableCrashInfo createFromParcel(Parcel in) {
+ return new ParcelableCrashInfo(in);
+ }
+
+ @Override
+ public ParcelableCrashInfo[] newArray(int size) {
+ return new ParcelableCrashInfo[size];
+ }
+ };
+ }
+
+ /**
+ * Describes an application not responding error.
+ */
+ public static class AnrInfo {
+ /**
+ * Activity name.
+ */
+ public String activity;
+
+ /**
+ * Description of the operation that timed out.
+ */
+ public String cause;
+
+ /**
+ * Additional info, including CPU stats.
+ */
+ public String info;
+
+ /**
+ * Create an uninitialized instance of AnrInfo.
+ */
+ public AnrInfo() {
+ }
+
+ /**
+ * Create an instance of AnrInfo initialized from a Parcel.
+ */
+ public AnrInfo(Parcel in) {
+ activity = in.readString();
+ cause = in.readString();
+ info = in.readString();
+ }
+
+ /**
+ * Save an AnrInfo instance to a parcel.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(activity);
+ dest.writeString(cause);
+ dest.writeString(info);
+ }
+
+ /**
+ * Dump an AnrInfo instance to a Printer.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "activity: " + activity);
+ pw.println(prefix + "cause: " + cause);
+ pw.println(prefix + "info: " + info);
+ }
+ }
+
+ /**
+ * Describes a battery usage report.
+ */
+ public static class BatteryInfo {
+ /**
+ * Percentage of the battery that was used up by the process.
+ */
+ public int usagePercent;
+
+ /**
+ * Duration in microseconds over which the process used the above
+ * percentage of battery.
+ */
+ public long durationMicros;
+
+ /**
+ * Dump of various info impacting battery use.
+ */
+ public String usageDetails;
+
+ /**
+ * Checkin details.
+ */
+ public String checkinDetails;
+
+ /**
+ * Create an uninitialized instance of BatteryInfo.
+ */
+ public BatteryInfo() {
+ }
+
+ /**
+ * Create an instance of BatteryInfo initialized from a Parcel.
+ */
+ public BatteryInfo(Parcel in) {
+ usagePercent = in.readInt();
+ durationMicros = in.readLong();
+ usageDetails = in.readString();
+ checkinDetails = in.readString();
+ }
+
+ /**
+ * Save a BatteryInfo instance to a parcel.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(usagePercent);
+ dest.writeLong(durationMicros);
+ dest.writeString(usageDetails);
+ dest.writeString(checkinDetails);
+ }
+
+ /**
+ * Dump a BatteryInfo instance to a Printer.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "usagePercent: " + usagePercent);
+ pw.println(prefix + "durationMicros: " + durationMicros);
+ pw.println(prefix + "usageDetails: " + usageDetails);
+ pw.println(prefix + "checkinDetails: " + checkinDetails);
+ }
+ }
+
+ /**
+ * Describes a running service report.
+ */
+ public static class RunningServiceInfo {
+ /**
+ * Duration in milliseconds that the service has been running.
+ */
+ public long durationMillis;
+
+ /**
+ * Dump of debug information about the service.
+ */
+ public String serviceDetails;
+
+ /**
+ * Create an uninitialized instance of RunningServiceInfo.
+ */
+ public RunningServiceInfo() {
+ }
+
+ /**
+ * Create an instance of RunningServiceInfo initialized from a Parcel.
+ */
+ public RunningServiceInfo(Parcel in) {
+ durationMillis = in.readLong();
+ serviceDetails = in.readString();
+ }
+
+ /**
+ * Save a RunningServiceInfo instance to a parcel.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(durationMillis);
+ dest.writeString(serviceDetails);
+ }
+
+ /**
+ * Dump a BatteryInfo instance to a Printer.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "durationMillis: " + durationMillis);
+ pw.println(prefix + "serviceDetails: " + serviceDetails);
+ }
+ }
+
+ public static final Parcelable.Creator<ApplicationErrorReport> CREATOR
+ = new Parcelable.Creator<ApplicationErrorReport>() {
+ public ApplicationErrorReport createFromParcel(Parcel source) {
+ return new ApplicationErrorReport(source);
+ }
+
+ public ApplicationErrorReport[] newArray(int size) {
+ return new ApplicationErrorReport[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Dump the report to a Printer.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "type: " + type);
+ pw.println(prefix + "packageName: " + packageName);
+ pw.println(prefix + "installerPackageName: " + installerPackageName);
+ pw.println(prefix + "processName: " + processName);
+ pw.println(prefix + "time: " + time);
+ pw.println(prefix + "systemApp: " + systemApp);
+
+ switch (type) {
+ case TYPE_CRASH:
+ crashInfo.dump(pw, prefix);
+ break;
+ case TYPE_ANR:
+ anrInfo.dump(pw, prefix);
+ break;
+ case TYPE_BATTERY:
+ batteryInfo.dump(pw, prefix);
+ break;
+ case TYPE_RUNNING_SERVICE:
+ runningServiceInfo.dump(pw, prefix);
+ break;
+ }
+ }
+}
diff --git a/android/app/ApplicationLoaders.java b/android/app/ApplicationLoaders.java
new file mode 100644
index 00000000..b7c1f4e0
--- /dev/null
+++ b/android/app/ApplicationLoaders.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Build;
+import android.os.Trace;
+import android.util.ArrayMap;
+import com.android.internal.os.ClassLoaderFactory;
+import dalvik.system.PathClassLoader;
+
+/** @hide */
+public class ApplicationLoaders {
+ public static ApplicationLoaders getDefault() {
+ return gApplicationLoaders;
+ }
+
+ ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
+ String librarySearchPath, String libraryPermittedPath,
+ ClassLoader parent, String classLoaderName) {
+ // For normal usage the cache key used is the same as the zip path.
+ return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath,
+ libraryPermittedPath, parent, zip, classLoaderName);
+ }
+
+ private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
+ String librarySearchPath, String libraryPermittedPath,
+ ClassLoader parent, String cacheKey,
+ String classLoaderName) {
+ /*
+ * This is the parent we use if they pass "null" in. In theory
+ * this should be the "system" class loader; in practice we
+ * don't use that and can happily (and more efficiently) use the
+ * bootstrap class loader.
+ */
+ ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
+
+ synchronized (mLoaders) {
+ if (parent == null) {
+ parent = baseParent;
+ }
+
+ /*
+ * If we're one step up from the base class loader, find
+ * something in our cache. Otherwise, we create a whole
+ * new ClassLoader for the zip archive.
+ */
+ if (parent == baseParent) {
+ ClassLoader loader = mLoaders.get(cacheKey);
+ if (loader != null) {
+ return loader;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
+
+ ClassLoader classloader = ClassLoaderFactory.createClassLoader(
+ zip, librarySearchPath, libraryPermittedPath, parent,
+ targetSdkVersion, isBundled, classLoaderName);
+
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setupVulkanLayerPath");
+ setupVulkanLayerPath(classloader, librarySearchPath);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ mLoaders.put(cacheKey, classloader);
+ return classloader;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
+ ClassLoader loader = ClassLoaderFactory.createClassLoader(
+ zip, null, parent, classLoaderName);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ return loader;
+ }
+ }
+
+ /**
+ * Creates a classloader for the WebView APK and places it in the cache of loaders maintained
+ * by this class. This is used in the WebView zygote, where its presence in the cache speeds up
+ * startup and enables memory sharing.
+ */
+ public ClassLoader createAndCacheWebViewClassLoader(String packagePath, String libsPath,
+ String cacheKey) {
+ // The correct paths are calculated by WebViewZygote in the system server and passed to
+ // us here. We hardcode the other parameters: WebView always targets the current SDK,
+ // does not need to use non-public system libraries, and uses the base classloader as its
+ // parent to permit usage of the cache.
+ // The cache key is passed separately to enable the stub WebView to be cached under the
+ // stub's APK path, when the actual package path is the donor APK.
+ return getClassLoader(packagePath, Build.VERSION.SDK_INT, false, libsPath, null, null,
+ cacheKey, null /* classLoaderName */);
+ }
+
+ private static native void setupVulkanLayerPath(ClassLoader classLoader, String librarySearchPath);
+
+ /**
+ * Adds a new path the classpath of the given loader.
+ * @throws IllegalStateException if the provided class loader is not a {@link PathClassLoader}.
+ */
+ void addPath(ClassLoader classLoader, String dexPath) {
+ if (!(classLoader instanceof PathClassLoader)) {
+ throw new IllegalStateException("class loader is not a PathClassLoader");
+ }
+ final PathClassLoader baseDexClassLoader = (PathClassLoader) classLoader;
+ baseDexClassLoader.addDexPath(dexPath);
+ }
+
+ private final ArrayMap<String, ClassLoader> mLoaders = new ArrayMap<>();
+
+ private static final ApplicationLoaders gApplicationLoaders = new ApplicationLoaders();
+}
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
new file mode 100644
index 00000000..0eafdec6
--- /dev/null
+++ b/android/app/ApplicationPackageManager.java
@@ -0,0 +1,2766 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.XmlRes;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ChangedPackages;
+import android.content.pm.ComponentInfo;
+import android.content.pm.FeatureInfo;
+import android.content.pm.IOnPermissionsChangeListener;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.IPackageInstallObserver;
+import android.content.pm.IPackageManager;
+import android.content.pm.IPackageMoveObserver;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.InstantAppInfo;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.IntentFilterVerificationInfo;
+import android.content.pm.KeySet;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageItemInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.VerifierDeviceIdentity;
+import android.content.pm.VersionedPackage;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructStat;
+import android.util.ArrayMap;
+import android.util.IconDrawableFactory;
+import android.util.LauncherIcons;
+import android.util.Log;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.UserIcons;
+
+import dalvik.system.VMRuntime;
+
+import libcore.util.EmptyArray;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** @hide */
+public class ApplicationPackageManager extends PackageManager {
+ private static final String TAG = "ApplicationPackageManager";
+ private final static boolean DEBUG_ICONS = false;
+
+ private static final int DEFAULT_EPHEMERAL_COOKIE_MAX_SIZE_BYTES = 16384; // 16KB
+
+ // Default flags to use with PackageManager when no flags are given.
+ private final static int sDefaultFlags = PackageManager.GET_SHARED_LIBRARY_FILES;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private UserManager mUserManager;
+ @GuardedBy("mLock")
+ private PackageInstaller mInstaller;
+
+ @GuardedBy("mDelegates")
+ private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private String mPermissionsControllerPackageName;
+
+ UserManager getUserManager() {
+ synchronized (mLock) {
+ if (mUserManager == null) {
+ mUserManager = UserManager.get(mContext);
+ }
+ return mUserManager;
+ }
+ }
+
+ @Override
+ public PackageInfo getPackageInfo(String packageName, int flags)
+ throws NameNotFoundException {
+ return getPackageInfoAsUser(packageName, flags, mContext.getUserId());
+ }
+
+ @Override
+ public PackageInfo getPackageInfo(VersionedPackage versionedPackage, int flags)
+ throws NameNotFoundException {
+ try {
+ PackageInfo pi = mPM.getPackageInfoVersioned(versionedPackage, flags,
+ mContext.getUserId());
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ throw new NameNotFoundException(versionedPackage.toString());
+ }
+
+ @Override
+ public PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId)
+ throws NameNotFoundException {
+ try {
+ PackageInfo pi = mPM.getPackageInfo(packageName, flags, userId);
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public String[] currentToCanonicalPackageNames(String[] names) {
+ try {
+ return mPM.currentToCanonicalPackageNames(names);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String[] canonicalToCurrentPackageNames(String[] names) {
+ try {
+ return mPM.canonicalToCurrentPackageNames(names);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public Intent getLaunchIntentForPackage(String packageName) {
+ // First see if the package has an INFO activity; the existence of
+ // such an activity is implied to be the desired front-door for the
+ // overall package (such as if it has multiple launcher entries).
+ Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
+ intentToResolve.addCategory(Intent.CATEGORY_INFO);
+ intentToResolve.setPackage(packageName);
+ List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);
+
+ // Otherwise, try to find a main launcher activity.
+ if (ris == null || ris.size() <= 0) {
+ // reuse the intent instance
+ intentToResolve.removeCategory(Intent.CATEGORY_INFO);
+ intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
+ intentToResolve.setPackage(packageName);
+ ris = queryIntentActivities(intentToResolve, 0);
+ }
+ if (ris == null || ris.size() <= 0) {
+ return null;
+ }
+ Intent intent = new Intent(intentToResolve);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName(ris.get(0).activityInfo.packageName,
+ ris.get(0).activityInfo.name);
+ return intent;
+ }
+
+ @Override
+ public Intent getLeanbackLaunchIntentForPackage(String packageName) {
+ // Try to find a main leanback_launcher activity.
+ Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
+ intentToResolve.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
+ intentToResolve.setPackage(packageName);
+ List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);
+
+ if (ris == null || ris.size() <= 0) {
+ return null;
+ }
+ Intent intent = new Intent(intentToResolve);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName(ris.get(0).activityInfo.packageName,
+ ris.get(0).activityInfo.name);
+ return intent;
+ }
+
+ @Override
+ public int[] getPackageGids(String packageName) throws NameNotFoundException {
+ return getPackageGids(packageName, 0);
+ }
+
+ @Override
+ public int[] getPackageGids(String packageName, int flags)
+ throws NameNotFoundException {
+ try {
+ int[] gids = mPM.getPackageGids(packageName, flags, mContext.getUserId());
+ if (gids != null) {
+ return gids;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public int getPackageUid(String packageName, int flags) throws NameNotFoundException {
+ return getPackageUidAsUser(packageName, flags, mContext.getUserId());
+ }
+
+ @Override
+ public int getPackageUidAsUser(String packageName, int userId) throws NameNotFoundException {
+ return getPackageUidAsUser(packageName, 0, userId);
+ }
+
+ @Override
+ public int getPackageUidAsUser(String packageName, int flags, int userId)
+ throws NameNotFoundException {
+ try {
+ int uid = mPM.getPackageUid(packageName, flags, userId);
+ if (uid >= 0) {
+ return uid;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public PermissionInfo getPermissionInfo(String name, int flags)
+ throws NameNotFoundException {
+ try {
+ PermissionInfo pi = mPM.getPermissionInfo(name,
+ mContext.getOpPackageName(), flags);
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(name);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<PermissionInfo> queryPermissionsByGroup(String group, int flags)
+ throws NameNotFoundException {
+ try {
+ ParceledListSlice<PermissionInfo> parceledList =
+ mPM.queryPermissionsByGroup(group, flags);
+ if (parceledList != null) {
+ List<PermissionInfo> pi = parceledList.getList();
+ if (pi != null) {
+ return pi;
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(group);
+ }
+
+ @Override
+ public boolean isPermissionReviewModeEnabled() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_permissionReviewRequired);
+ }
+
+ @Override
+ public PermissionGroupInfo getPermissionGroupInfo(String name,
+ int flags) throws NameNotFoundException {
+ try {
+ PermissionGroupInfo pgi = mPM.getPermissionGroupInfo(name, flags);
+ if (pgi != null) {
+ return pgi;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(name);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<PermissionGroupInfo> getAllPermissionGroups(int flags) {
+ try {
+ ParceledListSlice<PermissionGroupInfo> parceledList =
+ mPM.getAllPermissionGroups(flags);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo(String packageName, int flags)
+ throws NameNotFoundException {
+ return getApplicationInfoAsUser(packageName, flags, mContext.getUserId());
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfoAsUser(String packageName, int flags, int userId)
+ throws NameNotFoundException {
+ try {
+ ApplicationInfo ai = mPM.getApplicationInfo(packageName, flags, userId);
+ if (ai != null) {
+ // This is a temporary hack. Callers must use
+ // createPackageContext(packageName).getApplicationInfo() to
+ // get the right paths.
+ return maybeAdjustApplicationInfo(ai);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ private static ApplicationInfo maybeAdjustApplicationInfo(ApplicationInfo info) {
+ // If we're dealing with a multi-arch application that has both
+ // 32 and 64 bit shared libraries, we might need to choose the secondary
+ // depending on what the current runtime's instruction set is.
+ if (info.primaryCpuAbi != null && info.secondaryCpuAbi != null) {
+ final String runtimeIsa = VMRuntime.getRuntime().vmInstructionSet();
+
+ // Get the instruction set that the libraries of secondary Abi is supported.
+ // In presence of a native bridge this might be different than the one secondary Abi used.
+ String secondaryIsa = VMRuntime.getInstructionSet(info.secondaryCpuAbi);
+ final String secondaryDexCodeIsa = SystemProperties.get("ro.dalvik.vm.isa." + secondaryIsa);
+ secondaryIsa = secondaryDexCodeIsa.isEmpty() ? secondaryIsa : secondaryDexCodeIsa;
+
+ // If the runtimeIsa is the same as the primary isa, then we do nothing.
+ // Everything will be set up correctly because info.nativeLibraryDir will
+ // correspond to the right ISA.
+ if (runtimeIsa.equals(secondaryIsa)) {
+ ApplicationInfo modified = new ApplicationInfo(info);
+ modified.nativeLibraryDir = info.secondaryNativeLibraryDir;
+ return modified;
+ }
+ }
+ return info;
+ }
+
+ @Override
+ public ActivityInfo getActivityInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ActivityInfo ai = mPM.getActivityInfo(className, flags, mContext.getUserId());
+ if (ai != null) {
+ return ai;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public ActivityInfo getReceiverInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ActivityInfo ai = mPM.getReceiverInfo(className, flags, mContext.getUserId());
+ if (ai != null) {
+ return ai;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public ServiceInfo getServiceInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ServiceInfo si = mPM.getServiceInfo(className, flags, mContext.getUserId());
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public ProviderInfo getProviderInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ProviderInfo pi = mPM.getProviderInfo(className, flags, mContext.getUserId());
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public String[] getSystemSharedLibraryNames() {
+ try {
+ return mPM.getSystemSharedLibraryNames();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public @NonNull List<SharedLibraryInfo> getSharedLibraries(int flags) {
+ return getSharedLibrariesAsUser(flags, mContext.getUserId());
+ }
+
+ /** @hide */
+ @Override
+ @SuppressWarnings("unchecked")
+ public @NonNull List<SharedLibraryInfo> getSharedLibrariesAsUser(int flags, int userId) {
+ try {
+ ParceledListSlice<SharedLibraryInfo> sharedLibs = mPM.getSharedLibraries(
+ mContext.getOpPackageName(), flags, userId);
+ if (sharedLibs == null) {
+ return Collections.emptyList();
+ }
+ return sharedLibs.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public @NonNull String getServicesSystemSharedLibraryPackageName() {
+ try {
+ return mPM.getServicesSystemSharedLibraryPackageName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public @NonNull String getSharedSystemSharedLibraryPackageName() {
+ try {
+ return mPM.getSharedSystemSharedLibraryPackageName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public ChangedPackages getChangedPackages(int sequenceNumber) {
+ try {
+ return mPM.getChangedPackages(sequenceNumber, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public FeatureInfo[] getSystemAvailableFeatures() {
+ try {
+ ParceledListSlice<FeatureInfo> parceledList =
+ mPM.getSystemAvailableFeatures();
+ if (parceledList == null) {
+ return new FeatureInfo[0];
+ }
+ final List<FeatureInfo> list = parceledList.getList();
+ final FeatureInfo[] res = new FeatureInfo[list.size()];
+ for (int i = 0; i < res.length; i++) {
+ res[i] = list.get(i);
+ }
+ return res;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean hasSystemFeature(String name) {
+ return hasSystemFeature(name, 0);
+ }
+
+ @Override
+ public boolean hasSystemFeature(String name, int version) {
+ try {
+ return mPM.hasSystemFeature(name, version);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int checkPermission(String permName, String pkgName) {
+ try {
+ return mPM.checkPermission(permName, pkgName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean isPermissionRevokedByPolicy(String permName, String pkgName) {
+ try {
+ return mPM.isPermissionRevokedByPolicy(permName, pkgName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public String getPermissionControllerPackageName() {
+ synchronized (mLock) {
+ if (mPermissionsControllerPackageName == null) {
+ try {
+ mPermissionsControllerPackageName = mPM.getPermissionControllerPackageName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return mPermissionsControllerPackageName;
+ }
+ }
+
+ @Override
+ public boolean addPermission(PermissionInfo info) {
+ try {
+ return mPM.addPermission(info);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean addPermissionAsync(PermissionInfo info) {
+ try {
+ return mPM.addPermissionAsync(info);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void removePermission(String name) {
+ try {
+ mPM.removePermission(name);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void grantRuntimePermission(String packageName, String permissionName,
+ UserHandle user) {
+ try {
+ mPM.grantRuntimePermission(packageName, permissionName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void revokeRuntimePermission(String packageName, String permissionName,
+ UserHandle user) {
+ try {
+ mPM.revokeRuntimePermission(packageName, permissionName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getPermissionFlags(String permissionName, String packageName, UserHandle user) {
+ try {
+ return mPM.getPermissionFlags(permissionName, packageName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void updatePermissionFlags(String permissionName, String packageName,
+ int flagMask, int flagValues, UserHandle user) {
+ try {
+ mPM.updatePermissionFlags(permissionName, packageName, flagMask,
+ flagValues, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(String permission) {
+ try {
+ return mPM.shouldShowRequestPermissionRationale(permission,
+ mContext.getPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int checkSignatures(String pkg1, String pkg2) {
+ try {
+ return mPM.checkSignatures(pkg1, pkg2);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int checkSignatures(int uid1, int uid2) {
+ try {
+ return mPM.checkUidSignatures(uid1, uid2);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String[] getPackagesForUid(int uid) {
+ try {
+ return mPM.getPackagesForUid(uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String getNameForUid(int uid) {
+ try {
+ return mPM.getNameForUid(uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String[] getNamesForUids(int[] uids) {
+ try {
+ return mPM.getNamesForUids(uids);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getUidForSharedUser(String sharedUserName)
+ throws NameNotFoundException {
+ try {
+ int uid = mPM.getUidForSharedUser(sharedUserName);
+ if(uid != -1) {
+ return uid;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ throw new NameNotFoundException("No shared userid for user:"+sharedUserName);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<PackageInfo> getInstalledPackages(int flags) {
+ return getInstalledPackagesAsUser(flags, mContext.getUserId());
+ }
+
+ /** @hide */
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
+ try {
+ ParceledListSlice<PackageInfo> parceledList =
+ mPM.getInstalledPackages(flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<PackageInfo> getPackagesHoldingPermissions(
+ String[] permissions, int flags) {
+ final int userId = mContext.getUserId();
+ try {
+ ParceledListSlice<PackageInfo> parceledList =
+ mPM.getPackagesHoldingPermissions(permissions, flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<ApplicationInfo> getInstalledApplications(int flags) {
+ return getInstalledApplicationsAsUser(flags, mContext.getUserId());
+ }
+
+ /** @hide */
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) {
+ try {
+ ParceledListSlice<ApplicationInfo> parceledList =
+ mPM.getInstalledApplications(flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<InstantAppInfo> getInstantApps() {
+ try {
+ ParceledListSlice<InstantAppInfo> slice =
+ mPM.getInstantApps(mContext.getUserId());
+ if (slice != null) {
+ return slice.getList();
+ }
+ return Collections.emptyList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public Drawable getInstantAppIcon(String packageName) {
+ try {
+ Bitmap bitmap = mPM.getInstantAppIcon(
+ packageName, mContext.getUserId());
+ if (bitmap != null) {
+ return new BitmapDrawable(null, bitmap);
+ }
+ return null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean isInstantApp() {
+ return isInstantApp(mContext.getPackageName());
+ }
+
+ @Override
+ public boolean isInstantApp(String packageName) {
+ try {
+ return mPM.isInstantApp(packageName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public int getInstantAppCookieMaxBytes() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES,
+ DEFAULT_EPHEMERAL_COOKIE_MAX_SIZE_BYTES);
+ }
+
+ @Override
+ public int getInstantAppCookieMaxSize() {
+ return getInstantAppCookieMaxBytes();
+ }
+
+ @Override
+ public @NonNull byte[] getInstantAppCookie() {
+ try {
+ final byte[] cookie = mPM.getInstantAppCookie(
+ mContext.getPackageName(), mContext.getUserId());
+ if (cookie != null) {
+ return cookie;
+ } else {
+ return EmptyArray.BYTE;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void clearInstantAppCookie() {
+ updateInstantAppCookie(null);
+ }
+
+ @Override
+ public void updateInstantAppCookie(@NonNull byte[] cookie) {
+ if (cookie != null && cookie.length > getInstantAppCookieMaxBytes()) {
+ throw new IllegalArgumentException("instant cookie longer than "
+ + getInstantAppCookieMaxBytes());
+ }
+ try {
+ mPM.setInstantAppCookie(mContext.getPackageName(),
+ cookie, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean setInstantAppCookie(@NonNull byte[] cookie) {
+ try {
+ return mPM.setInstantAppCookie(mContext.getPackageName(),
+ cookie, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public ResolveInfo resolveActivity(Intent intent, int flags) {
+ return resolveActivityAsUser(intent, flags, mContext.getUserId());
+ }
+
+ @Override
+ public ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) {
+ try {
+ return mPM.resolveIntent(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags,
+ userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentActivities(Intent intent,
+ int flags) {
+ return queryIntentActivitiesAsUser(intent, flags, mContext.getUserId());
+ }
+
+ /** @hide Same as above but for a specific user */
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent,
+ int flags, int userId) {
+ try {
+ ParceledListSlice<ResolveInfo> parceledList =
+ mPM.queryIntentActivities(intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ResolveInfo> queryIntentActivityOptions(
+ ComponentName caller, Intent[] specifics, Intent intent,
+ int flags) {
+ final ContentResolver resolver = mContext.getContentResolver();
+
+ String[] specificTypes = null;
+ if (specifics != null) {
+ final int N = specifics.length;
+ for (int i=0; i<N; i++) {
+ Intent sp = specifics[i];
+ if (sp != null) {
+ String t = sp.resolveTypeIfNeeded(resolver);
+ if (t != null) {
+ if (specificTypes == null) {
+ specificTypes = new String[N];
+ }
+ specificTypes[i] = t;
+ }
+ }
+ }
+ }
+
+ try {
+ ParceledListSlice<ResolveInfo> parceledList =
+ mPM.queryIntentActivityOptions(caller, specifics, specificTypes, intent,
+ intent.resolveTypeIfNeeded(resolver), flags, mContext.getUserId());
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ResolveInfo> queryBroadcastReceiversAsUser(Intent intent, int flags, int userId) {
+ try {
+ ParceledListSlice<ResolveInfo> parceledList =
+ mPM.queryIntentReceivers(intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags) {
+ return queryBroadcastReceiversAsUser(intent, flags, mContext.getUserId());
+ }
+
+ @Override
+ public ResolveInfo resolveService(Intent intent, int flags) {
+ try {
+ return mPM.resolveService(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int flags, int userId) {
+ try {
+ ParceledListSlice<ResolveInfo> parceledList =
+ mPM.queryIntentServices(intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentServices(Intent intent, int flags) {
+ return queryIntentServicesAsUser(intent, flags, mContext.getUserId());
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ResolveInfo> queryIntentContentProvidersAsUser(
+ Intent intent, int flags, int userId) {
+ try {
+ ParceledListSlice<ResolveInfo> parceledList =
+ mPM.queryIntentContentProviders(intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags, userId);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentContentProviders(Intent intent, int flags) {
+ return queryIntentContentProvidersAsUser(intent, flags, mContext.getUserId());
+ }
+
+ @Override
+ public ProviderInfo resolveContentProvider(String name, int flags) {
+ return resolveContentProviderAsUser(name, flags, mContext.getUserId());
+ }
+
+ /** @hide **/
+ @Override
+ public ProviderInfo resolveContentProviderAsUser(String name, int flags, int userId) {
+ try {
+ return mPM.resolveContentProvider(name, flags, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public List<ProviderInfo> queryContentProviders(String processName,
+ int uid, int flags) {
+ return queryContentProviders(processName, uid, flags, null);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<ProviderInfo> queryContentProviders(String processName,
+ int uid, int flags, String metaDataKey) {
+ try {
+ ParceledListSlice<ProviderInfo> slice =
+ mPM.queryContentProviders(processName, uid, flags, metaDataKey);
+ return slice != null ? slice.getList() : Collections.<ProviderInfo>emptyList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public InstrumentationInfo getInstrumentationInfo(
+ ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ InstrumentationInfo ii = mPM.getInstrumentationInfo(
+ className, flags);
+ if (ii != null) {
+ return ii;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<InstrumentationInfo> queryInstrumentation(
+ String targetPackage, int flags) {
+ try {
+ ParceledListSlice<InstrumentationInfo> parceledList =
+ mPM.queryInstrumentation(targetPackage, flags);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Nullable
+ @Override
+ public Drawable getDrawable(String packageName, @DrawableRes int resId,
+ @Nullable ApplicationInfo appInfo) {
+ final ResourceName name = new ResourceName(packageName, resId);
+ final Drawable cachedIcon = getCachedIcon(name);
+ if (cachedIcon != null) {
+ return cachedIcon;
+ }
+
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, sDefaultFlags);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ if (resId != 0) {
+ try {
+ final Resources r = getResourcesForApplication(appInfo);
+ final Drawable dr = r.getDrawable(resId, null);
+ if (dr != null) {
+ putCachedIcon(name, dr);
+ }
+
+ if (false) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "Getting drawable 0x" + Integer.toHexString(resId)
+ + " from package " + packageName
+ + ": app scale=" + r.getCompatibilityInfo().applicationScale
+ + ", caller scale=" + mContext.getResources()
+ .getCompatibilityInfo().applicationScale,
+ e);
+ }
+ if (DEBUG_ICONS) {
+ Log.v(TAG, "Getting drawable 0x"
+ + Integer.toHexString(resId) + " from " + r
+ + ": " + dr);
+ }
+ return dr;
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for "
+ + appInfo.packageName);
+ } catch (Resources.NotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for "
+ + appInfo.packageName + ": " + e.getMessage());
+ } catch (Exception e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving icon 0x"
+ + Integer.toHexString(resId) + " in package "
+ + packageName, e);
+ }
+ }
+
+ return null;
+ }
+
+ @Override public Drawable getActivityIcon(ComponentName activityName)
+ throws NameNotFoundException {
+ return getActivityInfo(activityName, sDefaultFlags).loadIcon(this);
+ }
+
+ @Override public Drawable getActivityIcon(Intent intent)
+ throws NameNotFoundException {
+ if (intent.getComponent() != null) {
+ return getActivityIcon(intent.getComponent());
+ }
+
+ ResolveInfo info = resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ return info.activityInfo.loadIcon(this);
+ }
+
+ throw new NameNotFoundException(intent.toUri(0));
+ }
+
+ @Override public Drawable getDefaultActivityIcon() {
+ return Resources.getSystem().getDrawable(
+ com.android.internal.R.drawable.sym_def_app_icon);
+ }
+
+ @Override public Drawable getApplicationIcon(ApplicationInfo info) {
+ return info.loadIcon(this);
+ }
+
+ @Override public Drawable getApplicationIcon(String packageName)
+ throws NameNotFoundException {
+ return getApplicationIcon(getApplicationInfo(packageName, sDefaultFlags));
+ }
+
+ @Override
+ public Drawable getActivityBanner(ComponentName activityName)
+ throws NameNotFoundException {
+ return getActivityInfo(activityName, sDefaultFlags).loadBanner(this);
+ }
+
+ @Override
+ public Drawable getActivityBanner(Intent intent)
+ throws NameNotFoundException {
+ if (intent.getComponent() != null) {
+ return getActivityBanner(intent.getComponent());
+ }
+
+ ResolveInfo info = resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ return info.activityInfo.loadBanner(this);
+ }
+
+ throw new NameNotFoundException(intent.toUri(0));
+ }
+
+ @Override
+ public Drawable getApplicationBanner(ApplicationInfo info) {
+ return info.loadBanner(this);
+ }
+
+ @Override
+ public Drawable getApplicationBanner(String packageName)
+ throws NameNotFoundException {
+ return getApplicationBanner(getApplicationInfo(packageName, sDefaultFlags));
+ }
+
+ @Override
+ public Drawable getActivityLogo(ComponentName activityName)
+ throws NameNotFoundException {
+ return getActivityInfo(activityName, sDefaultFlags).loadLogo(this);
+ }
+
+ @Override
+ public Drawable getActivityLogo(Intent intent)
+ throws NameNotFoundException {
+ if (intent.getComponent() != null) {
+ return getActivityLogo(intent.getComponent());
+ }
+
+ ResolveInfo info = resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ return info.activityInfo.loadLogo(this);
+ }
+
+ throw new NameNotFoundException(intent.toUri(0));
+ }
+
+ @Override
+ public Drawable getApplicationLogo(ApplicationInfo info) {
+ return info.loadLogo(this);
+ }
+
+ @Override
+ public Drawable getApplicationLogo(String packageName)
+ throws NameNotFoundException {
+ return getApplicationLogo(getApplicationInfo(packageName, sDefaultFlags));
+ }
+
+ @Override
+ public Drawable getUserBadgedIcon(Drawable icon, UserHandle user) {
+ if (!isManagedProfile(user.getIdentifier())) {
+ return icon;
+ }
+ Drawable badge = new LauncherIcons(mContext).getBadgeDrawable(
+ com.android.internal.R.drawable.ic_corp_icon_badge_case,
+ getUserBadgeColor(user));
+ return getBadgedDrawable(icon, badge, null, true);
+ }
+
+ @Override
+ public Drawable getUserBadgedDrawableForDensity(Drawable drawable, UserHandle user,
+ Rect badgeLocation, int badgeDensity) {
+ Drawable badgeDrawable = getUserBadgeForDensity(user, badgeDensity);
+ if (badgeDrawable == null) {
+ return drawable;
+ }
+ return getBadgedDrawable(drawable, badgeDrawable, badgeLocation, true);
+ }
+
+ @VisibleForTesting
+ public static final int[] CORP_BADGE_LABEL_RES_ID = new int[] {
+ com.android.internal.R.string.managed_profile_label_badge,
+ com.android.internal.R.string.managed_profile_label_badge_2,
+ com.android.internal.R.string.managed_profile_label_badge_3
+ };
+
+ private int getUserBadgeColor(UserHandle user) {
+ return IconDrawableFactory.getUserBadgeColor(getUserManager(), user.getIdentifier());
+ }
+
+ @Override
+ public Drawable getUserBadgeForDensity(UserHandle user, int density) {
+ Drawable badgeColor = getManagedProfileIconForDensity(user,
+ com.android.internal.R.drawable.ic_corp_badge_color, density);
+ if (badgeColor == null) {
+ return null;
+ }
+ badgeColor.setTint(getUserBadgeColor(user));
+ Drawable badgeForeground = getDrawableForDensity(
+ com.android.internal.R.drawable.ic_corp_badge_case, density);
+ Drawable badge = new LayerDrawable(
+ new Drawable[] {badgeColor, badgeForeground });
+ return badge;
+ }
+
+ @Override
+ public Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density) {
+ Drawable badge = getManagedProfileIconForDensity(user,
+ com.android.internal.R.drawable.ic_corp_badge_no_background, density);
+ if (badge != null) {
+ badge.setTint(getUserBadgeColor(user));
+ }
+ return badge;
+ }
+
+ private Drawable getDrawableForDensity(int drawableId, int density) {
+ if (density <= 0) {
+ density = mContext.getResources().getDisplayMetrics().densityDpi;
+ }
+ return Resources.getSystem().getDrawableForDensity(drawableId, density);
+ }
+
+ private Drawable getManagedProfileIconForDensity(UserHandle user, int drawableId, int density) {
+ if (isManagedProfile(user.getIdentifier())) {
+ return getDrawableForDensity(drawableId, density);
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
+ if (isManagedProfile(user.getIdentifier())) {
+ int badge = getUserManager().getManagedProfileBadge(user.getIdentifier());
+ int resourceId = CORP_BADGE_LABEL_RES_ID[badge % CORP_BADGE_LABEL_RES_ID.length];
+ return Resources.getSystem().getString(resourceId, label);
+ }
+ return label;
+ }
+
+ @Override
+ public Resources getResourcesForActivity(ComponentName activityName)
+ throws NameNotFoundException {
+ return getResourcesForApplication(
+ getActivityInfo(activityName, sDefaultFlags).applicationInfo);
+ }
+
+ @Override
+ public Resources getResourcesForApplication(@NonNull ApplicationInfo app)
+ throws NameNotFoundException {
+ if (app.packageName.equals("system")) {
+ return mContext.mMainThread.getSystemUiContext().getResources();
+ }
+ final boolean sameUid = (app.uid == Process.myUid());
+ final Resources r = mContext.mMainThread.getTopLevelResources(
+ sameUid ? app.sourceDir : app.publicSourceDir,
+ sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
+ app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
+ mContext.mPackageInfo);
+ if (r != null) {
+ return r;
+ }
+ throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
+
+ }
+
+ @Override
+ public Resources getResourcesForApplication(String appPackageName)
+ throws NameNotFoundException {
+ return getResourcesForApplication(
+ getApplicationInfo(appPackageName, sDefaultFlags));
+ }
+
+ /** @hide */
+ @Override
+ public Resources getResourcesForApplicationAsUser(String appPackageName, int userId)
+ throws NameNotFoundException {
+ if (userId < 0) {
+ throw new IllegalArgumentException(
+ "Call does not support special user #" + userId);
+ }
+ if ("system".equals(appPackageName)) {
+ return mContext.mMainThread.getSystemUiContext().getResources();
+ }
+ try {
+ ApplicationInfo ai = mPM.getApplicationInfo(appPackageName, sDefaultFlags, userId);
+ if (ai != null) {
+ return getResourcesForApplication(ai);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ throw new NameNotFoundException("Package " + appPackageName + " doesn't exist");
+ }
+
+ volatile int mCachedSafeMode = -1;
+
+ @Override
+ public boolean isSafeMode() {
+ try {
+ if (mCachedSafeMode < 0) {
+ mCachedSafeMode = mPM.isSafeMode() ? 1 : 0;
+ }
+ return mCachedSafeMode != 0;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void addOnPermissionsChangeListener(OnPermissionsChangedListener listener) {
+ synchronized (mPermissionListeners) {
+ if (mPermissionListeners.get(listener) != null) {
+ return;
+ }
+ OnPermissionsChangeListenerDelegate delegate =
+ new OnPermissionsChangeListenerDelegate(listener, Looper.getMainLooper());
+ try {
+ mPM.addOnPermissionsChangeListener(delegate);
+ mPermissionListeners.put(listener, delegate);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ public void removeOnPermissionsChangeListener(OnPermissionsChangedListener listener) {
+ synchronized (mPermissionListeners) {
+ IOnPermissionsChangeListener delegate = mPermissionListeners.get(listener);
+ if (delegate != null) {
+ try {
+ mPM.removeOnPermissionsChangeListener(delegate);
+ mPermissionListeners.remove(listener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ static void configurationChanged() {
+ synchronized (sSync) {
+ sIconCache.clear();
+ sStringCache.clear();
+ }
+ }
+
+ protected ApplicationPackageManager(ContextImpl context,
+ IPackageManager pm) {
+ mContext = context;
+ mPM = pm;
+ }
+
+ @Nullable
+ private Drawable getCachedIcon(@NonNull ResourceName name) {
+ synchronized (sSync) {
+ final WeakReference<Drawable.ConstantState> wr = sIconCache.get(name);
+ if (DEBUG_ICONS) Log.v(TAG, "Get cached weak drawable ref for "
+ + name + ": " + wr);
+ if (wr != null) { // we have the activity
+ final Drawable.ConstantState state = wr.get();
+ if (state != null) {
+ if (DEBUG_ICONS) {
+ Log.v(TAG, "Get cached drawable state for " + name + ": " + state);
+ }
+ // Note: It's okay here to not use the newDrawable(Resources) variant
+ // of the API. The ConstantState comes from a drawable that was
+ // originally created by passing the proper app Resources instance
+ // which means the state should already contain the proper
+ // resources specific information (like density.) See
+ // BitmapDrawable.BitmapState for instance.
+ return state.newDrawable();
+ }
+ // our entry has been purged
+ sIconCache.remove(name);
+ }
+ }
+ return null;
+ }
+
+ private void putCachedIcon(@NonNull ResourceName name, @NonNull Drawable dr) {
+ synchronized (sSync) {
+ sIconCache.put(name, new WeakReference<>(dr.getConstantState()));
+ if (DEBUG_ICONS) Log.v(TAG, "Added cached drawable state for " + name + ": " + dr);
+ }
+ }
+
+ static void handlePackageBroadcast(int cmd, String[] pkgList, boolean hasPkgInfo) {
+ boolean immediateGc = false;
+ if (cmd == ApplicationThreadConstants.EXTERNAL_STORAGE_UNAVAILABLE) {
+ immediateGc = true;
+ }
+ if (pkgList != null && (pkgList.length > 0)) {
+ boolean needCleanup = false;
+ for (String ssp : pkgList) {
+ synchronized (sSync) {
+ for (int i=sIconCache.size()-1; i>=0; i--) {
+ ResourceName nm = sIconCache.keyAt(i);
+ if (nm.packageName.equals(ssp)) {
+ //Log.i(TAG, "Removing cached drawable for " + nm);
+ sIconCache.removeAt(i);
+ needCleanup = true;
+ }
+ }
+ for (int i=sStringCache.size()-1; i>=0; i--) {
+ ResourceName nm = sStringCache.keyAt(i);
+ if (nm.packageName.equals(ssp)) {
+ //Log.i(TAG, "Removing cached string for " + nm);
+ sStringCache.removeAt(i);
+ needCleanup = true;
+ }
+ }
+ }
+ }
+ if (needCleanup || hasPkgInfo) {
+ if (immediateGc) {
+ // Schedule an immediate gc.
+ Runtime.getRuntime().gc();
+ } else {
+ ActivityThread.currentActivityThread().scheduleGcIdler();
+ }
+ }
+ }
+ }
+
+ private static final class ResourceName {
+ final String packageName;
+ final int iconId;
+
+ ResourceName(String _packageName, int _iconId) {
+ packageName = _packageName;
+ iconId = _iconId;
+ }
+
+ ResourceName(ApplicationInfo aInfo, int _iconId) {
+ this(aInfo.packageName, _iconId);
+ }
+
+ ResourceName(ComponentInfo cInfo, int _iconId) {
+ this(cInfo.applicationInfo.packageName, _iconId);
+ }
+
+ ResourceName(ResolveInfo rInfo, int _iconId) {
+ this(rInfo.activityInfo.applicationInfo.packageName, _iconId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ResourceName that = (ResourceName) o;
+
+ if (iconId != that.iconId) return false;
+ return !(packageName != null ?
+ !packageName.equals(that.packageName) : that.packageName != null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ result = packageName.hashCode();
+ result = 31 * result + iconId;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "{ResourceName " + packageName + " / " + iconId + "}";
+ }
+ }
+
+ private CharSequence getCachedString(ResourceName name) {
+ synchronized (sSync) {
+ WeakReference<CharSequence> wr = sStringCache.get(name);
+ if (wr != null) { // we have the activity
+ CharSequence cs = wr.get();
+ if (cs != null) {
+ return cs;
+ }
+ // our entry has been purged
+ sStringCache.remove(name);
+ }
+ }
+ return null;
+ }
+
+ private void putCachedString(ResourceName name, CharSequence cs) {
+ synchronized (sSync) {
+ sStringCache.put(name, new WeakReference<CharSequence>(cs));
+ }
+ }
+
+ @Override
+ public CharSequence getText(String packageName, @StringRes int resid,
+ ApplicationInfo appInfo) {
+ ResourceName name = new ResourceName(packageName, resid);
+ CharSequence text = getCachedString(name);
+ if (text != null) {
+ return text;
+ }
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, sDefaultFlags);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+ try {
+ Resources r = getResourcesForApplication(appInfo);
+ text = r.getText(resid);
+ putCachedString(name, text);
+ return text;
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for "
+ + appInfo.packageName);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving text 0x"
+ + Integer.toHexString(resid) + " in package "
+ + packageName, e);
+ }
+ return null;
+ }
+
+ @Override
+ public XmlResourceParser getXml(String packageName, @XmlRes int resid,
+ ApplicationInfo appInfo) {
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, sDefaultFlags);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+ try {
+ Resources r = getResourcesForApplication(appInfo);
+ return r.getXml(resid);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving xml 0x"
+ + Integer.toHexString(resid) + " in package "
+ + packageName, e);
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for "
+ + appInfo.packageName);
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence getApplicationLabel(ApplicationInfo info) {
+ return info.loadLabel(this);
+ }
+
+ @Override
+ public void installPackage(Uri packageURI, IPackageInstallObserver observer, int flags,
+ String installerPackageName) {
+ installCommon(packageURI, new LegacyPackageInstallObserver(observer), flags,
+ installerPackageName, mContext.getUserId());
+ }
+
+ @Override
+ public void installPackage(Uri packageURI, PackageInstallObserver observer,
+ int flags, String installerPackageName) {
+ installCommon(packageURI, observer, flags, installerPackageName, mContext.getUserId());
+ }
+
+ private void installCommon(Uri packageURI,
+ PackageInstallObserver observer, int flags, String installerPackageName,
+ int userId) {
+ if (!"file".equals(packageURI.getScheme())) {
+ throw new UnsupportedOperationException("Only file:// URIs are supported");
+ }
+
+ final String originPath = packageURI.getPath();
+ try {
+ mPM.installPackageAsUser(originPath, observer.getBinder(), flags, installerPackageName,
+ userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int installExistingPackage(String packageName) throws NameNotFoundException {
+ return installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN);
+ }
+
+ @Override
+ public int installExistingPackage(String packageName, int installReason)
+ throws NameNotFoundException {
+ return installExistingPackageAsUser(packageName, installReason, mContext.getUserId());
+ }
+
+ @Override
+ public int installExistingPackageAsUser(String packageName, int userId)
+ throws NameNotFoundException {
+ return installExistingPackageAsUser(packageName, PackageManager.INSTALL_REASON_UNKNOWN,
+ userId);
+ }
+
+ private int installExistingPackageAsUser(String packageName, int installReason, int userId)
+ throws NameNotFoundException {
+ try {
+ int res = mPM.installExistingPackageAsUser(packageName, userId, 0 /*installFlags*/,
+ installReason);
+ if (res == INSTALL_FAILED_INVALID_URI) {
+ throw new NameNotFoundException("Package " + packageName + " doesn't exist");
+ }
+ return res;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void verifyPendingInstall(int id, int response) {
+ try {
+ mPM.verifyPendingInstall(id, response);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void extendVerificationTimeout(int id, int verificationCodeAtTimeout,
+ long millisecondsToDelay) {
+ try {
+ mPM.extendVerificationTimeout(id, verificationCodeAtTimeout, millisecondsToDelay);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void verifyIntentFilter(int id, int verificationCode, List<String> failedDomains) {
+ try {
+ mPM.verifyIntentFilter(id, verificationCode, failedDomains);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getIntentVerificationStatusAsUser(String packageName, int userId) {
+ try {
+ return mPM.getIntentVerificationStatus(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean updateIntentVerificationStatusAsUser(String packageName, int status, int userId) {
+ try {
+ return mPM.updateIntentVerificationStatus(packageName, status, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<IntentFilterVerificationInfo> getIntentFilterVerifications(String packageName) {
+ try {
+ ParceledListSlice<IntentFilterVerificationInfo> parceledList =
+ mPM.getIntentFilterVerifications(packageName);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<IntentFilter> getAllIntentFilters(String packageName) {
+ try {
+ ParceledListSlice<IntentFilter> parceledList =
+ mPM.getAllIntentFilters(packageName);
+ if (parceledList == null) {
+ return Collections.emptyList();
+ }
+ return parceledList.getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String getDefaultBrowserPackageNameAsUser(int userId) {
+ try {
+ return mPM.getDefaultBrowserPackageName(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId) {
+ try {
+ return mPM.setDefaultBrowserPackageName(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void setInstallerPackageName(String targetPackage,
+ String installerPackageName) {
+ try {
+ mPM.setInstallerPackageName(targetPackage, installerPackageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void setUpdateAvailable(String packageName, boolean updateAvailable) {
+ try {
+ mPM.setUpdateAvailable(packageName, updateAvailable);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String getInstallerPackageName(String packageName) {
+ try {
+ return mPM.getInstallerPackageName(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getMoveStatus(int moveId) {
+ try {
+ return mPM.getMoveStatus(moveId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void registerMoveCallback(MoveCallback callback, Handler handler) {
+ synchronized (mDelegates) {
+ final MoveCallbackDelegate delegate = new MoveCallbackDelegate(callback,
+ handler.getLooper());
+ try {
+ mPM.registerMoveCallback(delegate);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mDelegates.add(delegate);
+ }
+ }
+
+ @Override
+ public void unregisterMoveCallback(MoveCallback callback) {
+ synchronized (mDelegates) {
+ for (Iterator<MoveCallbackDelegate> i = mDelegates.iterator(); i.hasNext();) {
+ final MoveCallbackDelegate delegate = i.next();
+ if (delegate.mCallback == callback) {
+ try {
+ mPM.unregisterMoveCallback(delegate);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ i.remove();
+ }
+ }
+ }
+ }
+
+ @Override
+ public int movePackage(String packageName, VolumeInfo vol) {
+ try {
+ final String volumeUuid;
+ if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) {
+ volumeUuid = StorageManager.UUID_PRIVATE_INTERNAL;
+ } else if (vol.isPrimaryPhysical()) {
+ volumeUuid = StorageManager.UUID_PRIMARY_PHYSICAL;
+ } else {
+ volumeUuid = Preconditions.checkNotNull(vol.fsUuid);
+ }
+
+ return mPM.movePackage(packageName, volumeUuid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app) {
+ final StorageManager storage = mContext.getSystemService(StorageManager.class);
+ return getPackageCurrentVolume(app, storage);
+ }
+
+ @VisibleForTesting
+ protected @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app,
+ StorageManager storage) {
+ if (app.isInternal()) {
+ return storage.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
+ } else if (app.isExternalAsec()) {
+ return storage.getPrimaryPhysicalVolume();
+ } else {
+ return storage.findVolumeByUuid(app.volumeUuid);
+ }
+ }
+
+ @Override
+ public @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app) {
+ final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
+ return getPackageCandidateVolumes(app, storageManager, mPM);
+ }
+
+ @VisibleForTesting
+ protected @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app,
+ StorageManager storageManager, IPackageManager pm) {
+ final VolumeInfo currentVol = getPackageCurrentVolume(app, storageManager);
+ final List<VolumeInfo> vols = storageManager.getVolumes();
+ final List<VolumeInfo> candidates = new ArrayList<>();
+ for (VolumeInfo vol : vols) {
+ if (Objects.equals(vol, currentVol)
+ || isPackageCandidateVolume(mContext, app, vol, pm)) {
+ candidates.add(vol);
+ }
+ }
+ return candidates;
+ }
+
+ @VisibleForTesting
+ protected boolean isForceAllowOnExternal(Context context) {
+ return Settings.Global.getInt(
+ context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
+ }
+
+ @VisibleForTesting
+ protected boolean isAllow3rdPartyOnInternal(Context context) {
+ return context.getResources().getBoolean(
+ com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
+ }
+
+ private boolean isPackageCandidateVolume(
+ ContextImpl context, ApplicationInfo app, VolumeInfo vol, IPackageManager pm) {
+ final boolean forceAllowOnExternal = isForceAllowOnExternal(context);
+
+ if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) {
+ return app.isSystemApp() || isAllow3rdPartyOnInternal(context);
+ }
+
+ // System apps and apps demanding internal storage can't be moved
+ // anywhere else
+ if (app.isSystemApp()) {
+ return false;
+ }
+ if (!forceAllowOnExternal
+ && (app.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY
+ || app.installLocation == PackageInfo.INSTALL_LOCATION_UNSPECIFIED)) {
+ return false;
+ }
+
+ // Gotta be able to write there
+ if (!vol.isMountedWritable()) {
+ return false;
+ }
+
+ // Moving into an ASEC on public primary is only option internal
+ if (vol.isPrimaryPhysical()) {
+ return app.isInternal();
+ }
+
+ // Some apps can't be moved. (e.g. device admins)
+ try {
+ if (pm.isPackageDeviceAdminOnAnyUser(app.packageName)) {
+ return false;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ // Otherwise we can move to any private volume
+ return (vol.getType() == VolumeInfo.TYPE_PRIVATE);
+ }
+
+ @Override
+ public int movePrimaryStorage(VolumeInfo vol) {
+ try {
+ final String volumeUuid;
+ if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) {
+ volumeUuid = StorageManager.UUID_PRIVATE_INTERNAL;
+ } else if (vol.isPrimaryPhysical()) {
+ volumeUuid = StorageManager.UUID_PRIMARY_PHYSICAL;
+ } else {
+ volumeUuid = Preconditions.checkNotNull(vol.fsUuid);
+ }
+
+ return mPM.movePrimaryStorage(volumeUuid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public @Nullable VolumeInfo getPrimaryStorageCurrentVolume() {
+ final StorageManager storage = mContext.getSystemService(StorageManager.class);
+ final String volumeUuid = storage.getPrimaryStorageUuid();
+ return storage.findVolumeByQualifiedUuid(volumeUuid);
+ }
+
+ @Override
+ public @NonNull List<VolumeInfo> getPrimaryStorageCandidateVolumes() {
+ final StorageManager storage = mContext.getSystemService(StorageManager.class);
+ final VolumeInfo currentVol = getPrimaryStorageCurrentVolume();
+ final List<VolumeInfo> vols = storage.getVolumes();
+ final List<VolumeInfo> candidates = new ArrayList<>();
+ if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL,
+ storage.getPrimaryStorageUuid()) && currentVol != null) {
+ // TODO: support moving primary physical to emulated volume
+ candidates.add(currentVol);
+ } else {
+ for (VolumeInfo vol : vols) {
+ if (Objects.equals(vol, currentVol) || isPrimaryStorageCandidateVolume(vol)) {
+ candidates.add(vol);
+ }
+ }
+ }
+ return candidates;
+ }
+
+ private static boolean isPrimaryStorageCandidateVolume(VolumeInfo vol) {
+ // Private internal is always an option
+ if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) {
+ return true;
+ }
+
+ // Gotta be able to write there
+ if (!vol.isMountedWritable()) {
+ return false;
+ }
+
+ // We can move to any private volume
+ return (vol.getType() == VolumeInfo.TYPE_PRIVATE);
+ }
+
+ @Override
+ public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {
+ deletePackageAsUser(packageName, observer, flags, mContext.getUserId());
+ }
+
+ @Override
+ public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer,
+ int flags, int userId) {
+ try {
+ mPM.deletePackageAsUser(packageName, PackageManager.VERSION_CODE_HIGHEST,
+ observer, userId, flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void clearApplicationUserData(String packageName,
+ IPackageDataObserver observer) {
+ try {
+ mPM.clearApplicationUserData(packageName, observer, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ @Override
+ public void deleteApplicationCacheFiles(String packageName,
+ IPackageDataObserver observer) {
+ try {
+ mPM.deleteApplicationCacheFiles(packageName, observer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void deleteApplicationCacheFilesAsUser(String packageName, int userId,
+ IPackageDataObserver observer) {
+ try {
+ mPM.deleteApplicationCacheFilesAsUser(packageName, userId, observer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void freeStorageAndNotify(String volumeUuid, long idealStorageSize,
+ IPackageDataObserver observer) {
+ try {
+ mPM.freeStorageAndNotify(volumeUuid, idealStorageSize, 0, observer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void freeStorage(String volumeUuid, long freeStorageSize, IntentSender pi) {
+ try {
+ mPM.freeStorage(volumeUuid, freeStorageSize, 0, pi);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public String[] setPackagesSuspendedAsUser(String[] packageNames, boolean suspended,
+ int userId) {
+ try {
+ return mPM.setPackagesSuspendedAsUser(packageNames, suspended, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean isPackageSuspendedForUser(String packageName, int userId) {
+ try {
+ return mPM.isPackageSuspendedForUser(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void setApplicationCategoryHint(String packageName, int categoryHint) {
+ try {
+ mPM.setApplicationCategoryHint(packageName, categoryHint,
+ mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void getPackageSizeInfoAsUser(String packageName, int userHandle,
+ IPackageStatsObserver observer) {
+ final String msg = "Shame on you for calling the hidden API "
+ + "getPackageSizeInfoAsUser(). Shame!";
+ if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) {
+ throw new UnsupportedOperationException(msg);
+ } else if (observer != null) {
+ Log.d(TAG, msg);
+ try {
+ observer.onGetStatsCompleted(null, false);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
+ @Override
+ public void addPackageToPreferred(String packageName) {
+ Log.w(TAG, "addPackageToPreferred() is a no-op");
+ }
+
+ @Override
+ public void removePackageFromPreferred(String packageName) {
+ Log.w(TAG, "removePackageFromPreferred() is a no-op");
+ }
+
+ @Override
+ public List<PackageInfo> getPreferredPackages(int flags) {
+ Log.w(TAG, "getPreferredPackages() is a no-op");
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void addPreferredActivity(IntentFilter filter,
+ int match, ComponentName[] set, ComponentName activity) {
+ try {
+ mPM.addPreferredActivity(filter, match, set, activity, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void addPreferredActivityAsUser(IntentFilter filter, int match,
+ ComponentName[] set, ComponentName activity, int userId) {
+ try {
+ mPM.addPreferredActivity(filter, match, set, activity, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void replacePreferredActivity(IntentFilter filter,
+ int match, ComponentName[] set, ComponentName activity) {
+ try {
+ mPM.replacePreferredActivity(filter, match, set, activity, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void replacePreferredActivityAsUser(IntentFilter filter,
+ int match, ComponentName[] set, ComponentName activity,
+ int userId) {
+ try {
+ mPM.replacePreferredActivity(filter, match, set, activity, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void clearPackagePreferredActivities(String packageName) {
+ try {
+ mPM.clearPackagePreferredActivities(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getPreferredActivities(List<IntentFilter> outFilters,
+ List<ComponentName> outActivities, String packageName) {
+ try {
+ return mPM.getPreferredActivities(outFilters, outActivities, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public ComponentName getHomeActivities(List<ResolveInfo> outActivities) {
+ try {
+ return mPM.getHomeActivities(outActivities);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void setComponentEnabledSetting(ComponentName componentName,
+ int newState, int flags) {
+ try {
+ mPM.setComponentEnabledSetting(componentName, newState, flags, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getComponentEnabledSetting(ComponentName componentName) {
+ try {
+ return mPM.getComponentEnabledSetting(componentName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void setApplicationEnabledSetting(String packageName,
+ int newState, int flags) {
+ try {
+ mPM.setApplicationEnabledSetting(packageName, newState, flags,
+ mContext.getUserId(), mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int getApplicationEnabledSetting(String packageName) {
+ try {
+ return mPM.getApplicationEnabledSetting(packageName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void flushPackageRestrictionsAsUser(int userId) {
+ try {
+ mPM.flushPackageRestrictionsAsUser(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean setApplicationHiddenSettingAsUser(String packageName, boolean hidden,
+ UserHandle user) {
+ try {
+ return mPM.setApplicationHiddenSettingAsUser(packageName, hidden,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean getApplicationHiddenSettingAsUser(String packageName, UserHandle user) {
+ try {
+ return mPM.getApplicationHiddenSettingAsUser(packageName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public KeySet getKeySetByAlias(String packageName, String alias) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(alias);
+ try {
+ return mPM.getKeySetByAlias(packageName, alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public KeySet getSigningKeySet(String packageName) {
+ Preconditions.checkNotNull(packageName);
+ try {
+ return mPM.getSigningKeySet(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean isSignedBy(String packageName, KeySet ks) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(ks);
+ try {
+ return mPM.isPackageSignedByKeySet(packageName, ks);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean isSignedByExactly(String packageName, KeySet ks) {
+ Preconditions.checkNotNull(packageName);
+ Preconditions.checkNotNull(ks);
+ try {
+ return mPM.isPackageSignedByKeySetExactly(packageName, ks);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public VerifierDeviceIdentity getVerifierDeviceIdentity() {
+ try {
+ return mPM.getVerifierDeviceIdentity();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean isUpgrade() {
+ try {
+ return mPM.isUpgrade();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public PackageInstaller getPackageInstaller() {
+ synchronized (mLock) {
+ if (mInstaller == null) {
+ try {
+ mInstaller = new PackageInstaller(mPM.getPackageInstaller(),
+ mContext.getPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return mInstaller;
+ }
+ }
+
+ @Override
+ public boolean isPackageAvailable(String packageName) {
+ try {
+ return mPM.isPackageAvailable(packageName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void addCrossProfileIntentFilter(IntentFilter filter, int sourceUserId, int targetUserId,
+ int flags) {
+ try {
+ mPM.addCrossProfileIntentFilter(filter, mContext.getOpPackageName(),
+ sourceUserId, targetUserId, flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void clearCrossProfileIntentFilters(int sourceUserId) {
+ try {
+ mPM.clearCrossProfileIntentFilters(sourceUserId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public Drawable loadItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) {
+ Drawable dr = loadUnbadgedItemIcon(itemInfo, appInfo);
+ if (itemInfo.showUserIcon != UserHandle.USER_NULL) {
+ return dr;
+ }
+ return getUserBadgedIcon(dr, new UserHandle(mContext.getUserId()));
+ }
+
+ /**
+ * @hide
+ */
+ public Drawable loadUnbadgedItemIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo) {
+ if (itemInfo.showUserIcon != UserHandle.USER_NULL) {
+ Bitmap bitmap = getUserManager().getUserIcon(itemInfo.showUserIcon);
+ if (bitmap == null) {
+ return UserIcons.getDefaultUserIcon(itemInfo.showUserIcon, /* light= */ false);
+ }
+ return new BitmapDrawable(bitmap);
+ }
+ Drawable dr = null;
+ if (itemInfo.packageName != null) {
+ dr = getDrawable(itemInfo.packageName, itemInfo.icon, appInfo);
+ }
+ if (dr == null) {
+ dr = itemInfo.loadDefaultIcon(this);
+ }
+ return dr;
+ }
+
+ private Drawable getBadgedDrawable(Drawable drawable, Drawable badgeDrawable,
+ Rect badgeLocation, boolean tryBadgeInPlace) {
+ final int badgedWidth = drawable.getIntrinsicWidth();
+ final int badgedHeight = drawable.getIntrinsicHeight();
+ final boolean canBadgeInPlace = tryBadgeInPlace
+ && (drawable instanceof BitmapDrawable)
+ && ((BitmapDrawable) drawable).getBitmap().isMutable();
+
+ final Bitmap bitmap;
+ if (canBadgeInPlace) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ bitmap = Bitmap.createBitmap(badgedWidth, badgedHeight, Bitmap.Config.ARGB_8888);
+ }
+ Canvas canvas = new Canvas(bitmap);
+
+ if (!canBadgeInPlace) {
+ drawable.setBounds(0, 0, badgedWidth, badgedHeight);
+ drawable.draw(canvas);
+ }
+
+ if (badgeLocation != null) {
+ if (badgeLocation.left < 0 || badgeLocation.top < 0
+ || badgeLocation.width() > badgedWidth || badgeLocation.height() > badgedHeight) {
+ throw new IllegalArgumentException("Badge location " + badgeLocation
+ + " not in badged drawable bounds "
+ + new Rect(0, 0, badgedWidth, badgedHeight));
+ }
+ badgeDrawable.setBounds(0, 0, badgeLocation.width(), badgeLocation.height());
+
+ canvas.save();
+ canvas.translate(badgeLocation.left, badgeLocation.top);
+ badgeDrawable.draw(canvas);
+ canvas.restore();
+ } else {
+ badgeDrawable.setBounds(0, 0, badgedWidth, badgedHeight);
+ badgeDrawable.draw(canvas);
+ }
+
+ if (!canBadgeInPlace) {
+ BitmapDrawable mergedDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
+
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ mergedDrawable.setTargetDensity(bitmapDrawable.getBitmap().getDensity());
+ }
+
+ return mergedDrawable;
+ }
+
+ return drawable;
+ }
+
+ private boolean isManagedProfile(int userId) {
+ return getUserManager().isManagedProfile(userId);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int getInstallReason(String packageName, UserHandle user) {
+ try {
+ return mPM.getInstallReason(packageName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ private static class MoveCallbackDelegate extends IPackageMoveObserver.Stub implements
+ Handler.Callback {
+ private static final int MSG_CREATED = 1;
+ private static final int MSG_STATUS_CHANGED = 2;
+
+ final MoveCallback mCallback;
+ final Handler mHandler;
+
+ public MoveCallbackDelegate(MoveCallback callback, Looper looper) {
+ mCallback = callback;
+ mHandler = new Handler(looper, this);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CREATED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ mCallback.onCreated(args.argi1, (Bundle) args.arg2);
+ args.recycle();
+ return true;
+ }
+ case MSG_STATUS_CHANGED: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ mCallback.onStatusChanged(args.argi1, args.argi2, (long) args.arg3);
+ args.recycle();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreated(int moveId, Bundle extras) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = moveId;
+ args.arg2 = extras;
+ mHandler.obtainMessage(MSG_CREATED, args).sendToTarget();
+ }
+
+ @Override
+ public void onStatusChanged(int moveId, int status, long estMillis) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = moveId;
+ args.argi2 = status;
+ args.arg3 = estMillis;
+ mHandler.obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget();
+ }
+ }
+
+ private final ContextImpl mContext;
+ private final IPackageManager mPM;
+
+ private static final Object sSync = new Object();
+ private static ArrayMap<ResourceName, WeakReference<Drawable.ConstantState>> sIconCache
+ = new ArrayMap<ResourceName, WeakReference<Drawable.ConstantState>>();
+ private static ArrayMap<ResourceName, WeakReference<CharSequence>> sStringCache
+ = new ArrayMap<ResourceName, WeakReference<CharSequence>>();
+
+ private final Map<OnPermissionsChangedListener, IOnPermissionsChangeListener>
+ mPermissionListeners = new ArrayMap<>();
+
+ public class OnPermissionsChangeListenerDelegate extends IOnPermissionsChangeListener.Stub
+ implements Handler.Callback{
+ private static final int MSG_PERMISSIONS_CHANGED = 1;
+
+ private final OnPermissionsChangedListener mListener;
+ private final Handler mHandler;
+
+
+ public OnPermissionsChangeListenerDelegate(OnPermissionsChangedListener listener,
+ Looper looper) {
+ mListener = listener;
+ mHandler = new Handler(looper, this);
+ }
+
+ @Override
+ public void onPermissionsChanged(int uid) {
+ mHandler.obtainMessage(MSG_PERMISSIONS_CHANGED, uid, 0).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PERMISSIONS_CHANGED: {
+ final int uid = msg.arg1;
+ mListener.onPermissionsChanged(uid);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canRequestPackageInstalls() {
+ try {
+ return mPM.canRequestPackageInstalls(mContext.getPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public ComponentName getInstantAppResolverSettingsComponent() {
+ try {
+ return mPM.getInstantAppResolverSettingsComponent();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public ComponentName getInstantAppInstallerComponent() {
+ try {
+ return mPM.getInstantAppInstallerComponent();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public String getInstantAppAndroidId(String packageName, UserHandle user) {
+ try {
+ return mPM.getInstantAppAndroidId(packageName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ private static class DexModuleRegisterResult {
+ final String dexModulePath;
+ final boolean success;
+ final String message;
+
+ private DexModuleRegisterResult(String dexModulePath, boolean success, String message) {
+ this.dexModulePath = dexModulePath;
+ this.success = success;
+ this.message = message;
+ }
+ }
+
+ private static class DexModuleRegisterCallbackDelegate
+ extends android.content.pm.IDexModuleRegisterCallback.Stub
+ implements Handler.Callback {
+ private static final int MSG_DEX_MODULE_REGISTERED = 1;
+ private final DexModuleRegisterCallback callback;
+ private final Handler mHandler;
+
+ DexModuleRegisterCallbackDelegate(@NonNull DexModuleRegisterCallback callback) {
+ this.callback = callback;
+ mHandler = new Handler(Looper.getMainLooper(), this);
+ }
+
+ @Override
+ public void onDexModuleRegistered(@NonNull String dexModulePath, boolean success,
+ @Nullable String message)throws RemoteException {
+ mHandler.obtainMessage(MSG_DEX_MODULE_REGISTERED,
+ new DexModuleRegisterResult(dexModulePath, success, message)).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what != MSG_DEX_MODULE_REGISTERED) {
+ return false;
+ }
+ DexModuleRegisterResult result = (DexModuleRegisterResult)msg.obj;
+ callback.onDexModuleRegistered(result.dexModulePath, result.success, result.message);
+ return true;
+ }
+ }
+
+ @Override
+ public void registerDexModule(@NonNull String dexModule,
+ @Nullable DexModuleRegisterCallback callback) {
+ // Check if this is a shared module by looking if the others can read it.
+ boolean isSharedModule = false;
+ try {
+ StructStat stat = Os.stat(dexModule);
+ if ((OsConstants.S_IROTH & stat.st_mode) != 0) {
+ isSharedModule = true;
+ }
+ } catch (ErrnoException e) {
+ callback.onDexModuleRegistered(dexModule, false,
+ "Could not get stat the module file: " + e.getMessage());
+ return;
+ }
+
+ // Module path is ok.
+ // Create the callback delegate to be passed to package manager service.
+ DexModuleRegisterCallbackDelegate callbackDelegate = null;
+ if (callback != null) {
+ callbackDelegate = new DexModuleRegisterCallbackDelegate(callback);
+ }
+
+ // Invoke the package manager service.
+ try {
+ mPM.registerDexModule(mContext.getPackageName(), dexModule,
+ isSharedModule, callbackDelegate);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+}
diff --git a/android/app/ApplicationThreadConstants.java b/android/app/ApplicationThreadConstants.java
new file mode 100644
index 00000000..1fa670fe
--- /dev/null
+++ b/android/app/ApplicationThreadConstants.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+/**
+ * @hide
+ */
+public final class ApplicationThreadConstants {
+ public static final int BACKUP_MODE_INCREMENTAL = 0;
+ public static final int BACKUP_MODE_FULL = 1;
+ public static final int BACKUP_MODE_RESTORE = 2;
+ public static final int BACKUP_MODE_RESTORE_FULL = 3;
+
+ public static final int DEBUG_OFF = 0;
+ public static final int DEBUG_ON = 1;
+ public static final int DEBUG_WAIT = 2;
+
+ // the package has been removed, clean up internal references
+ public static final int PACKAGE_REMOVED = 0;
+ public static final int EXTERNAL_STORAGE_UNAVAILABLE = 1;
+ // the package is being modified in-place, don't kill it and retain references to it
+ public static final int PACKAGE_REMOVED_DONT_KILL = 2;
+ // a previously removed package was replaced with a new version [eg. upgrade, split added, ...]
+ public static final int PACKAGE_REPLACED = 3;
+} \ No newline at end of file
diff --git a/android/app/AuthenticationRequiredException.java b/android/app/AuthenticationRequiredException.java
new file mode 100644
index 00000000..04e5e0a8
--- /dev/null
+++ b/android/app/AuthenticationRequiredException.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Specialization of {@link SecurityException} that is thrown when authentication is needed from the
+ * end user before viewing the content.
+ * <p>
+ * This exception is only appropriate where there is a concrete action the user can take to
+ * authorize and make forward progress, such as confirming or entering authentication credentials,
+ * or granting access via other means.
+ * <p class="note">
+ * Note: legacy code that receives this exception may treat it as a general
+ * {@link SecurityException}, and thus there is no guarantee that the action contained will be
+ * invoked by the user.
+ * </p>
+ */
+public final class AuthenticationRequiredException extends SecurityException implements Parcelable {
+ private static final String TAG = "AuthenticationRequiredException";
+
+ private final PendingIntent mUserAction;
+
+ /** {@hide} */
+ public AuthenticationRequiredException(Parcel in) {
+ this(new SecurityException(in.readString()), PendingIntent.CREATOR.createFromParcel(in));
+ }
+
+ /**
+ * Create an instance ready to be thrown.
+ *
+ * @param cause original cause with details designed for engineering
+ * audiences.
+ * @param userAction primary action that will initiate the recovery. This
+ * must launch an activity that is expected to set
+ * {@link Activity#setResult(int)} before finishing to
+ * communicate the final status of the recovery. For example,
+ * apps that observe {@link Activity#RESULT_OK} may choose to
+ * immediately retry their operation.
+ */
+ public AuthenticationRequiredException(Throwable cause, PendingIntent userAction) {
+ super(cause.getMessage());
+ mUserAction = Preconditions.checkNotNull(userAction);
+ }
+
+ /**
+ * Return primary action that will initiate the authorization.
+ */
+ public PendingIntent getUserAction() {
+ return mUserAction;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(getMessage());
+ mUserAction.writeToParcel(dest, flags);
+ }
+
+ public static final Creator<AuthenticationRequiredException> CREATOR =
+ new Creator<AuthenticationRequiredException>() {
+ @Override
+ public AuthenticationRequiredException createFromParcel(Parcel source) {
+ return new AuthenticationRequiredException(source);
+ }
+
+ @Override
+ public AuthenticationRequiredException[] newArray(int size) {
+ return new AuthenticationRequiredException[size];
+ }
+ };
+}
diff --git a/android/app/AutomaticZenRule.java b/android/app/AutomaticZenRule.java
new file mode 100644
index 00000000..cd4ace66
--- /dev/null
+++ b/android/app/AutomaticZenRule.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2015, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.app.NotificationManager.InterruptionFilter;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Rule instance information for zen mode.
+ */
+public final class AutomaticZenRule implements Parcelable {
+
+ private boolean enabled = false;
+ private String name;
+ private @InterruptionFilter int interruptionFilter;
+ private Uri conditionId;
+ private ComponentName owner;
+ private long creationTime;
+
+ /**
+ * Creates an automatic zen rule.
+ *
+ * @param name The name of the rule.
+ * @param owner The Condition Provider service that owns this rule.
+ * @param conditionId A representation of the state that should cause the Condition Provider
+ * service to apply the given interruption filter.
+ * @param interruptionFilter The interruption filter defines which notifications are allowed to
+ * interrupt the user (e.g. via sound &amp; vibration) while this rule
+ * is active.
+ * @param enabled Whether the rule is enabled.
+ */
+ public AutomaticZenRule(String name, ComponentName owner, Uri conditionId,
+ int interruptionFilter, boolean enabled) {
+ this.name = name;
+ this.owner = owner;
+ this.conditionId = conditionId;
+ this.interruptionFilter = interruptionFilter;
+ this.enabled = enabled;
+ }
+
+ /**
+ * @SystemApi
+ * @hide
+ */
+ public AutomaticZenRule(String name, ComponentName owner, Uri conditionId,
+ int interruptionFilter, boolean enabled, long creationTime) {
+ this(name, owner, conditionId, interruptionFilter, enabled);
+ this.creationTime = creationTime;
+ }
+
+ public AutomaticZenRule(Parcel source) {
+ enabled = source.readInt() == 1;
+ if (source.readInt() == 1) {
+ name = source.readString();
+ }
+ interruptionFilter = source.readInt();
+ conditionId = source.readParcelable(null);
+ owner = source.readParcelable(null);
+ creationTime = source.readLong();
+ }
+
+ /**
+ * Returns the {@link ComponentName} of the condition provider service that owns this rule.
+ */
+ public ComponentName getOwner() {
+ return owner;
+ }
+
+ /**
+ * Returns the representation of the state that causes this rule to become active.
+ */
+ public Uri getConditionId() {
+ return conditionId;
+ }
+
+ /**
+ * Returns the interruption filter that is applied when this rule is active.
+ */
+ public int getInterruptionFilter() {
+ return interruptionFilter;
+ }
+
+ /**
+ * Returns the name of this rule.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns whether this rule is enabled.
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Returns the time this rule was created, represented as milliseconds since the epoch.
+ */
+ public long getCreationTime() {
+ return creationTime;
+ }
+
+ /**
+ * Sets the representation of the state that causes this rule to become active.
+ */
+ public void setConditionId(Uri conditionId) {
+ this.conditionId = conditionId;
+ }
+
+ /**
+ * Sets the interruption filter that is applied when this rule is active.
+ * @param interruptionFilter The do not disturb mode to enter when this rule is active.
+ */
+ public void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
+ this.interruptionFilter = interruptionFilter;
+ }
+
+ /**
+ * Sets the name of this rule.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Enables this rule.
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(enabled ? 1 : 0);
+ if (name != null) {
+ dest.writeInt(1);
+ dest.writeString(name);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(interruptionFilter);
+ dest.writeParcelable(conditionId, 0);
+ dest.writeParcelable(owner, 0);
+ dest.writeLong(creationTime);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[')
+ .append("enabled=").append(enabled)
+ .append(",name=").append(name)
+ .append(",interruptionFilter=").append(interruptionFilter)
+ .append(",conditionId=").append(conditionId)
+ .append(",owner=").append(owner)
+ .append(",creationTime=").append(creationTime)
+ .append(']').toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AutomaticZenRule)) return false;
+ if (o == this) return true;
+ final AutomaticZenRule other = (AutomaticZenRule) o;
+ return other.enabled == enabled
+ && Objects.equals(other.name, name)
+ && other.interruptionFilter == interruptionFilter
+ && Objects.equals(other.conditionId, conditionId)
+ && Objects.equals(other.owner, owner)
+ && other.creationTime == creationTime;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, creationTime);
+ }
+
+ public static final Parcelable.Creator<AutomaticZenRule> CREATOR
+ = new Parcelable.Creator<AutomaticZenRule>() {
+ @Override
+ public AutomaticZenRule createFromParcel(Parcel source) {
+ return new AutomaticZenRule(source);
+ }
+ @Override
+ public AutomaticZenRule[] newArray(int size) {
+ return new AutomaticZenRule[size];
+ }
+ };
+}
diff --git a/android/app/BackStackRecord.java b/android/app/BackStackRecord.java
new file mode 100644
index 00000000..46e6defb
--- /dev/null
+++ b/android/app/BackStackRecord.java
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LogWriter;
+import android.view.View;
+
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+
+final class BackStackState implements Parcelable {
+ final int[] mOps;
+ final int mTransition;
+ final int mTransitionStyle;
+ final String mName;
+ final int mIndex;
+ final int mBreadCrumbTitleRes;
+ final CharSequence mBreadCrumbTitleText;
+ final int mBreadCrumbShortTitleRes;
+ final CharSequence mBreadCrumbShortTitleText;
+ final ArrayList<String> mSharedElementSourceNames;
+ final ArrayList<String> mSharedElementTargetNames;
+ final boolean mReorderingAllowed;
+
+ public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) {
+ final int numOps = bse.mOps.size();
+ mOps = new int[numOps * 6];
+
+ if (!bse.mAddToBackStack) {
+ throw new IllegalStateException("Not on back stack");
+ }
+
+ int pos = 0;
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final BackStackRecord.Op op = bse.mOps.get(opNum);
+ mOps[pos++] = op.cmd;
+ mOps[pos++] = op.fragment != null ? op.fragment.mIndex : -1;
+ mOps[pos++] = op.enterAnim;
+ mOps[pos++] = op.exitAnim;
+ mOps[pos++] = op.popEnterAnim;
+ mOps[pos++] = op.popExitAnim;
+ }
+ mTransition = bse.mTransition;
+ mTransitionStyle = bse.mTransitionStyle;
+ mName = bse.mName;
+ mIndex = bse.mIndex;
+ mBreadCrumbTitleRes = bse.mBreadCrumbTitleRes;
+ mBreadCrumbTitleText = bse.mBreadCrumbTitleText;
+ mBreadCrumbShortTitleRes = bse.mBreadCrumbShortTitleRes;
+ mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText;
+ mSharedElementSourceNames = bse.mSharedElementSourceNames;
+ mSharedElementTargetNames = bse.mSharedElementTargetNames;
+ mReorderingAllowed = bse.mReorderingAllowed;
+ }
+
+ public BackStackState(Parcel in) {
+ mOps = in.createIntArray();
+ mTransition = in.readInt();
+ mTransitionStyle = in.readInt();
+ mName = in.readString();
+ mIndex = in.readInt();
+ mBreadCrumbTitleRes = in.readInt();
+ mBreadCrumbTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mBreadCrumbShortTitleRes = in.readInt();
+ mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mSharedElementSourceNames = in.createStringArrayList();
+ mSharedElementTargetNames = in.createStringArrayList();
+ mReorderingAllowed = in.readInt() != 0;
+ }
+
+ public BackStackRecord instantiate(FragmentManagerImpl fm) {
+ BackStackRecord bse = new BackStackRecord(fm);
+ int pos = 0;
+ int num = 0;
+ while (pos < mOps.length) {
+ BackStackRecord.Op op = new BackStackRecord.Op();
+ op.cmd = mOps[pos++];
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(FragmentManagerImpl.TAG,
+ "Instantiate " + bse + " op #" + num + " base fragment #" + mOps[pos]);
+ }
+ int findex = mOps[pos++];
+ if (findex >= 0) {
+ Fragment f = fm.mActive.get(findex);
+ op.fragment = f;
+ } else {
+ op.fragment = null;
+ }
+ op.enterAnim = mOps[pos++];
+ op.exitAnim = mOps[pos++];
+ op.popEnterAnim = mOps[pos++];
+ op.popExitAnim = mOps[pos++];
+ bse.mEnterAnim = op.enterAnim;
+ bse.mExitAnim = op.exitAnim;
+ bse.mPopEnterAnim = op.popEnterAnim;
+ bse.mPopExitAnim = op.popExitAnim;
+ bse.addOp(op);
+ num++;
+ }
+ bse.mTransition = mTransition;
+ bse.mTransitionStyle = mTransitionStyle;
+ bse.mName = mName;
+ bse.mIndex = mIndex;
+ bse.mAddToBackStack = true;
+ bse.mBreadCrumbTitleRes = mBreadCrumbTitleRes;
+ bse.mBreadCrumbTitleText = mBreadCrumbTitleText;
+ bse.mBreadCrumbShortTitleRes = mBreadCrumbShortTitleRes;
+ bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText;
+ bse.mSharedElementSourceNames = mSharedElementSourceNames;
+ bse.mSharedElementTargetNames = mSharedElementTargetNames;
+ bse.mReorderingAllowed = mReorderingAllowed;
+ bse.bumpBackStackNesting(1);
+ return bse;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mOps);
+ dest.writeInt(mTransition);
+ dest.writeInt(mTransitionStyle);
+ dest.writeString(mName);
+ dest.writeInt(mIndex);
+ dest.writeInt(mBreadCrumbTitleRes);
+ TextUtils.writeToParcel(mBreadCrumbTitleText, dest, 0);
+ dest.writeInt(mBreadCrumbShortTitleRes);
+ TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0);
+ dest.writeStringList(mSharedElementSourceNames);
+ dest.writeStringList(mSharedElementTargetNames);
+ dest.writeInt(mReorderingAllowed ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<BackStackState> CREATOR
+ = new Parcelable.Creator<BackStackState>() {
+ public BackStackState createFromParcel(Parcel in) {
+ return new BackStackState(in);
+ }
+
+ public BackStackState[] newArray(int size) {
+ return new BackStackState[size];
+ }
+ };
+}
+
+/**
+ * @hide Entry of an operation on the fragment back stack.
+ */
+final class BackStackRecord extends FragmentTransaction implements
+ FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {
+ static final String TAG = FragmentManagerImpl.TAG;
+
+ final FragmentManagerImpl mManager;
+
+ static final int OP_NULL = 0;
+ static final int OP_ADD = 1;
+ static final int OP_REPLACE = 2;
+ static final int OP_REMOVE = 3;
+ static final int OP_HIDE = 4;
+ static final int OP_SHOW = 5;
+ static final int OP_DETACH = 6;
+ static final int OP_ATTACH = 7;
+ static final int OP_SET_PRIMARY_NAV = 8;
+ static final int OP_UNSET_PRIMARY_NAV = 9;
+
+ static final class Op {
+ int cmd;
+ Fragment fragment;
+ int enterAnim;
+ int exitAnim;
+ int popEnterAnim;
+ int popExitAnim;
+
+ Op() {
+ }
+
+ Op(int cmd, Fragment fragment) {
+ this.cmd = cmd;
+ this.fragment = fragment;
+ }
+ }
+
+ ArrayList<Op> mOps = new ArrayList<>();
+ int mEnterAnim;
+ int mExitAnim;
+ int mPopEnterAnim;
+ int mPopExitAnim;
+ int mTransition;
+ int mTransitionStyle;
+ boolean mAddToBackStack;
+ boolean mAllowAddToBackStack = true;
+ String mName;
+ boolean mCommitted;
+ int mIndex = -1;
+ boolean mReorderingAllowed;
+
+ ArrayList<Runnable> mCommitRunnables;
+
+ int mBreadCrumbTitleRes;
+ CharSequence mBreadCrumbTitleText;
+ int mBreadCrumbShortTitleRes;
+ CharSequence mBreadCrumbShortTitleText;
+
+ ArrayList<String> mSharedElementSourceNames;
+ ArrayList<String> mSharedElementTargetNames;
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("BackStackEntry{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ if (mIndex >= 0) {
+ sb.append(" #");
+ sb.append(mIndex);
+ }
+ if (mName != null) {
+ sb.append(" ");
+ sb.append(mName);
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ dump(prefix, writer, true);
+ }
+
+ void dump(String prefix, PrintWriter writer, boolean full) {
+ if (full) {
+ writer.print(prefix);
+ writer.print("mName=");
+ writer.print(mName);
+ writer.print(" mIndex=");
+ writer.print(mIndex);
+ writer.print(" mCommitted=");
+ writer.println(mCommitted);
+ if (mTransition != FragmentTransaction.TRANSIT_NONE) {
+ writer.print(prefix);
+ writer.print("mTransition=#");
+ writer.print(Integer.toHexString(mTransition));
+ writer.print(" mTransitionStyle=#");
+ writer.println(Integer.toHexString(mTransitionStyle));
+ }
+ if (mEnterAnim != 0 || mExitAnim != 0) {
+ writer.print(prefix);
+ writer.print("mEnterAnim=#");
+ writer.print(Integer.toHexString(mEnterAnim));
+ writer.print(" mExitAnim=#");
+ writer.println(Integer.toHexString(mExitAnim));
+ }
+ if (mPopEnterAnim != 0 || mPopExitAnim != 0) {
+ writer.print(prefix);
+ writer.print("mPopEnterAnim=#");
+ writer.print(Integer.toHexString(mPopEnterAnim));
+ writer.print(" mPopExitAnim=#");
+ writer.println(Integer.toHexString(mPopExitAnim));
+ }
+ if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) {
+ writer.print(prefix);
+ writer.print("mBreadCrumbTitleRes=#");
+ writer.print(Integer.toHexString(mBreadCrumbTitleRes));
+ writer.print(" mBreadCrumbTitleText=");
+ writer.println(mBreadCrumbTitleText);
+ }
+ if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) {
+ writer.print(prefix);
+ writer.print("mBreadCrumbShortTitleRes=#");
+ writer.print(Integer.toHexString(mBreadCrumbShortTitleRes));
+ writer.print(" mBreadCrumbShortTitleText=");
+ writer.println(mBreadCrumbShortTitleText);
+ }
+ }
+
+ if (!mOps.isEmpty()) {
+ writer.print(prefix);
+ writer.println("Operations:");
+ String innerPrefix = prefix + " ";
+ final int numOps = mOps.size();
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final Op op = mOps.get(opNum);
+ String cmdStr;
+ switch (op.cmd) {
+ case OP_NULL:
+ cmdStr = "NULL";
+ break;
+ case OP_ADD:
+ cmdStr = "ADD";
+ break;
+ case OP_REPLACE:
+ cmdStr = "REPLACE";
+ break;
+ case OP_REMOVE:
+ cmdStr = "REMOVE";
+ break;
+ case OP_HIDE:
+ cmdStr = "HIDE";
+ break;
+ case OP_SHOW:
+ cmdStr = "SHOW";
+ break;
+ case OP_DETACH:
+ cmdStr = "DETACH";
+ break;
+ case OP_ATTACH:
+ cmdStr = "ATTACH";
+ break;
+ case OP_SET_PRIMARY_NAV:
+ cmdStr="SET_PRIMARY_NAV";
+ break;
+ case OP_UNSET_PRIMARY_NAV:
+ cmdStr="UNSET_PRIMARY_NAV";
+ break;
+
+ default:
+ cmdStr = "cmd=" + op.cmd;
+ break;
+ }
+ writer.print(prefix);
+ writer.print(" Op #");
+ writer.print(opNum);
+ writer.print(": ");
+ writer.print(cmdStr);
+ writer.print(" ");
+ writer.println(op.fragment);
+ if (full) {
+ if (op.enterAnim != 0 || op.exitAnim != 0) {
+ writer.print(innerPrefix);
+ writer.print("enterAnim=#");
+ writer.print(Integer.toHexString(op.enterAnim));
+ writer.print(" exitAnim=#");
+ writer.println(Integer.toHexString(op.exitAnim));
+ }
+ if (op.popEnterAnim != 0 || op.popExitAnim != 0) {
+ writer.print(innerPrefix);
+ writer.print("popEnterAnim=#");
+ writer.print(Integer.toHexString(op.popEnterAnim));
+ writer.print(" popExitAnim=#");
+ writer.println(Integer.toHexString(op.popExitAnim));
+ }
+ }
+ }
+ }
+ }
+
+ public BackStackRecord(FragmentManagerImpl manager) {
+ mManager = manager;
+ mReorderingAllowed = mManager.getTargetSdk() > Build.VERSION_CODES.N_MR1;
+ }
+
+ public int getId() {
+ return mIndex;
+ }
+
+ public int getBreadCrumbTitleRes() {
+ return mBreadCrumbTitleRes;
+ }
+
+ public int getBreadCrumbShortTitleRes() {
+ return mBreadCrumbShortTitleRes;
+ }
+
+ public CharSequence getBreadCrumbTitle() {
+ if (mBreadCrumbTitleRes != 0 && mManager.mHost != null) {
+ return mManager.mHost.getContext().getText(mBreadCrumbTitleRes);
+ }
+ return mBreadCrumbTitleText;
+ }
+
+ public CharSequence getBreadCrumbShortTitle() {
+ if (mBreadCrumbShortTitleRes != 0 && mManager.mHost != null) {
+ return mManager.mHost.getContext().getText(mBreadCrumbShortTitleRes);
+ }
+ return mBreadCrumbShortTitleText;
+ }
+
+ void addOp(Op op) {
+ mOps.add(op);
+ op.enterAnim = mEnterAnim;
+ op.exitAnim = mExitAnim;
+ op.popEnterAnim = mPopEnterAnim;
+ op.popExitAnim = mPopExitAnim;
+ }
+
+ public FragmentTransaction add(Fragment fragment, String tag) {
+ doAddOp(0, fragment, tag, OP_ADD);
+ return this;
+ }
+
+ public FragmentTransaction add(int containerViewId, Fragment fragment) {
+ doAddOp(containerViewId, fragment, null, OP_ADD);
+ return this;
+ }
+
+ public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) {
+ doAddOp(containerViewId, fragment, tag, OP_ADD);
+ return this;
+ }
+
+ private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
+ if (mManager.getTargetSdk() > Build.VERSION_CODES.N_MR1) {
+ final Class fragmentClass = fragment.getClass();
+ final int modifiers = fragmentClass.getModifiers();
+ if ((fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)
+ || (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers)))) {
+ throw new IllegalStateException("Fragment " + fragmentClass.getCanonicalName()
+ + " must be a public static class to be properly recreated from"
+ + " instance state.");
+ }
+ }
+ fragment.mFragmentManager = mManager;
+
+ if (tag != null) {
+ if (fragment.mTag != null && !tag.equals(fragment.mTag)) {
+ throw new IllegalStateException("Can't change tag of fragment "
+ + fragment + ": was " + fragment.mTag
+ + " now " + tag);
+ }
+ fragment.mTag = tag;
+ }
+
+ if (containerViewId != 0) {
+ if (containerViewId == View.NO_ID) {
+ throw new IllegalArgumentException("Can't add fragment "
+ + fragment + " with tag " + tag + " to container view with no id");
+ }
+ if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
+ throw new IllegalStateException("Can't change container ID of fragment "
+ + fragment + ": was " + fragment.mFragmentId
+ + " now " + containerViewId);
+ }
+ fragment.mContainerId = fragment.mFragmentId = containerViewId;
+ }
+
+ addOp(new Op(opcmd, fragment));
+ }
+
+ public FragmentTransaction replace(int containerViewId, Fragment fragment) {
+ return replace(containerViewId, fragment, null);
+ }
+
+ public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
+ if (containerViewId == 0) {
+ throw new IllegalArgumentException("Must use non-zero containerViewId");
+ }
+
+ doAddOp(containerViewId, fragment, tag, OP_REPLACE);
+ return this;
+ }
+
+ public FragmentTransaction remove(Fragment fragment) {
+ addOp(new Op(OP_REMOVE, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction hide(Fragment fragment) {
+ addOp(new Op(OP_HIDE, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction show(Fragment fragment) {
+ addOp(new Op(OP_SHOW, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction detach(Fragment fragment) {
+ addOp(new Op(OP_DETACH, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction attach(Fragment fragment) {
+ addOp(new Op(OP_ATTACH, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction setPrimaryNavigationFragment(Fragment fragment) {
+ addOp(new Op(OP_SET_PRIMARY_NAV, fragment));
+
+ return this;
+ }
+
+ public FragmentTransaction setCustomAnimations(int enter, int exit) {
+ return setCustomAnimations(enter, exit, 0, 0);
+ }
+
+ public FragmentTransaction setCustomAnimations(int enter, int exit,
+ int popEnter, int popExit) {
+ mEnterAnim = enter;
+ mExitAnim = exit;
+ mPopEnterAnim = popEnter;
+ mPopExitAnim = popExit;
+ return this;
+ }
+
+ public FragmentTransaction setTransition(int transition) {
+ mTransition = transition;
+ return this;
+ }
+
+ @Override
+ public FragmentTransaction addSharedElement(View sharedElement, String name) {
+ String transitionName = sharedElement.getTransitionName();
+ if (transitionName == null) {
+ throw new IllegalArgumentException("Unique transitionNames are required for all" +
+ " sharedElements");
+ }
+ if (mSharedElementSourceNames == null) {
+ mSharedElementSourceNames = new ArrayList<String>();
+ mSharedElementTargetNames = new ArrayList<String>();
+ } else if (mSharedElementTargetNames.contains(name)) {
+ throw new IllegalArgumentException("A shared element with the target name '"
+ + name + "' has already been added to the transaction.");
+ } else if (mSharedElementSourceNames.contains(transitionName)) {
+ throw new IllegalArgumentException("A shared element with the source name '"
+ + transitionName + " has already been added to the transaction.");
+ }
+ mSharedElementSourceNames.add(transitionName);
+ mSharedElementTargetNames.add(name);
+ return this;
+ }
+
+ public FragmentTransaction setTransitionStyle(int styleRes) {
+ mTransitionStyle = styleRes;
+ return this;
+ }
+
+ public FragmentTransaction addToBackStack(String name) {
+ if (!mAllowAddToBackStack) {
+ throw new IllegalStateException(
+ "This FragmentTransaction is not allowed to be added to the back stack.");
+ }
+ mAddToBackStack = true;
+ mName = name;
+ return this;
+ }
+
+ public boolean isAddToBackStackAllowed() {
+ return mAllowAddToBackStack;
+ }
+
+ public FragmentTransaction disallowAddToBackStack() {
+ if (mAddToBackStack) {
+ throw new IllegalStateException(
+ "This transaction is already being added to the back stack");
+ }
+ mAllowAddToBackStack = false;
+ return this;
+ }
+
+ public FragmentTransaction setBreadCrumbTitle(int res) {
+ mBreadCrumbTitleRes = res;
+ mBreadCrumbTitleText = null;
+ return this;
+ }
+
+ public FragmentTransaction setBreadCrumbTitle(CharSequence text) {
+ mBreadCrumbTitleRes = 0;
+ mBreadCrumbTitleText = text;
+ return this;
+ }
+
+ public FragmentTransaction setBreadCrumbShortTitle(int res) {
+ mBreadCrumbShortTitleRes = res;
+ mBreadCrumbShortTitleText = null;
+ return this;
+ }
+
+ public FragmentTransaction setBreadCrumbShortTitle(CharSequence text) {
+ mBreadCrumbShortTitleRes = 0;
+ mBreadCrumbShortTitleText = text;
+ return this;
+ }
+
+ void bumpBackStackNesting(int amt) {
+ if (!mAddToBackStack) {
+ return;
+ }
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(TAG, "Bump nesting in " + this
+ + " by " + amt);
+ }
+ final int numOps = mOps.size();
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final Op op = mOps.get(opNum);
+ if (op.fragment != null) {
+ op.fragment.mBackStackNesting += amt;
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(TAG, "Bump nesting of "
+ + op.fragment + " to " + op.fragment.mBackStackNesting);
+ }
+ }
+ }
+ }
+
+ @Override
+ public FragmentTransaction runOnCommit(Runnable runnable) {
+ if (runnable == null) {
+ throw new IllegalArgumentException("runnable cannot be null");
+ }
+ disallowAddToBackStack();
+ if (mCommitRunnables == null) {
+ mCommitRunnables = new ArrayList<>();
+ }
+ mCommitRunnables.add(runnable);
+ return this;
+ }
+
+ public void runOnCommitRunnables() {
+ if (mCommitRunnables != null) {
+ for (int i = 0, N = mCommitRunnables.size(); i < N; i++) {
+ mCommitRunnables.get(i).run();
+ }
+ mCommitRunnables = null;
+ }
+ }
+
+ public int commit() {
+ return commitInternal(false);
+ }
+
+ public int commitAllowingStateLoss() {
+ return commitInternal(true);
+ }
+
+ @Override
+ public void commitNow() {
+ disallowAddToBackStack();
+ mManager.execSingleAction(this, false);
+ }
+
+ @Override
+ public void commitNowAllowingStateLoss() {
+ disallowAddToBackStack();
+ mManager.execSingleAction(this, true);
+ }
+
+ @Override
+ public FragmentTransaction setReorderingAllowed(boolean reorderingAllowed) {
+ mReorderingAllowed = reorderingAllowed;
+ return this;
+ }
+
+ int commitInternal(boolean allowStateLoss) {
+ if (mCommitted) {
+ throw new IllegalStateException("commit already called");
+ }
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(TAG, "Commit: " + this);
+ LogWriter logw = new LogWriter(Log.VERBOSE, TAG);
+ PrintWriter pw = new FastPrintWriter(logw, false, 1024);
+ dump(" ", null, pw, null);
+ pw.flush();
+ }
+ mCommitted = true;
+ if (mAddToBackStack) {
+ mIndex = mManager.allocBackStackIndex(this);
+ } else {
+ mIndex = -1;
+ }
+ mManager.enqueueAction(this, allowStateLoss);
+ return mIndex;
+ }
+
+ /**
+ * Implementation of {@link android.app.FragmentManagerImpl.OpGenerator}.
+ * This operation is added to the list of pending actions during {@link #commit()}, and
+ * will be executed on the UI thread to run this FragmentTransaction.
+ *
+ * @param records Modified to add this BackStackRecord
+ * @param isRecordPop Modified to add a false (this isn't a pop)
+ * @return true always because the records and isRecordPop will always be changed
+ */
+ @Override
+ public boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop) {
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(TAG, "Run: " + this);
+ }
+
+ records.add(this);
+ isRecordPop.add(false);
+ if (mAddToBackStack) {
+ mManager.addBackStackState(this);
+ }
+ return true;
+ }
+
+ boolean interactsWith(int containerId) {
+ final int numOps = mOps.size();
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final Op op = mOps.get(opNum);
+ final int fragContainer = op.fragment != null ? op.fragment.mContainerId : 0;
+ if (fragContainer != 0 && fragContainer == containerId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean interactsWith(ArrayList<BackStackRecord> records, int startIndex, int endIndex) {
+ if (endIndex == startIndex) {
+ return false;
+ }
+ final int numOps = mOps.size();
+ int lastContainer = -1;
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final Op op = mOps.get(opNum);
+ final int container = op.fragment != null ? op.fragment.mContainerId : 0;
+ if (container != 0 && container != lastContainer) {
+ lastContainer = container;
+ for (int i = startIndex; i < endIndex; i++) {
+ BackStackRecord record = records.get(i);
+ final int numThoseOps = record.mOps.size();
+ for (int thoseOpIndex = 0; thoseOpIndex < numThoseOps; thoseOpIndex++) {
+ final Op thatOp = record.mOps.get(thoseOpIndex);
+ final int thatContainer = thatOp.fragment != null
+ ? thatOp.fragment.mContainerId : 0;
+ if (thatContainer == container) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Executes the operations contained within this transaction. The Fragment states will only
+ * be modified if optimizations are not allowed.
+ */
+ void executeOps() {
+ final int numOps = mOps.size();
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final Op op = mOps.get(opNum);
+ final Fragment f = op.fragment;
+ if (f != null) {
+ f.setNextTransition(mTransition, mTransitionStyle);
+ }
+ switch (op.cmd) {
+ case OP_ADD:
+ f.setNextAnim(op.enterAnim);
+ mManager.addFragment(f, false);
+ break;
+ case OP_REMOVE:
+ f.setNextAnim(op.exitAnim);
+ mManager.removeFragment(f);
+ break;
+ case OP_HIDE:
+ f.setNextAnim(op.exitAnim);
+ mManager.hideFragment(f);
+ break;
+ case OP_SHOW:
+ f.setNextAnim(op.enterAnim);
+ mManager.showFragment(f);
+ break;
+ case OP_DETACH:
+ f.setNextAnim(op.exitAnim);
+ mManager.detachFragment(f);
+ break;
+ case OP_ATTACH:
+ f.setNextAnim(op.enterAnim);
+ mManager.attachFragment(f);
+ break;
+ case OP_SET_PRIMARY_NAV:
+ mManager.setPrimaryNavigationFragment(f);
+ break;
+ case OP_UNSET_PRIMARY_NAV:
+ mManager.setPrimaryNavigationFragment(null);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
+ }
+ if (!mReorderingAllowed && op.cmd != OP_ADD && f != null) {
+ mManager.moveFragmentToExpectedState(f);
+ }
+ }
+ if (!mReorderingAllowed) {
+ // Added fragments are added at the end to comply with prior behavior.
+ mManager.moveToState(mManager.mCurState, true);
+ }
+ }
+
+ /**
+ * Reverses the execution of the operations within this transaction. The Fragment states will
+ * only be modified if optimizations are not allowed.
+ *
+ * @param moveToState {@code true} if added fragments should be moved to their final state
+ * in unoptimized transactions
+ */
+ void executePopOps(boolean moveToState) {
+ for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
+ final Op op = mOps.get(opNum);
+ Fragment f = op.fragment;
+ if (f != null) {
+ f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),
+ mTransitionStyle);
+ }
+ switch (op.cmd) {
+ case OP_ADD:
+ f.setNextAnim(op.popExitAnim);
+ mManager.removeFragment(f);
+ break;
+ case OP_REMOVE:
+ f.setNextAnim(op.popEnterAnim);
+ mManager.addFragment(f, false);
+ break;
+ case OP_HIDE:
+ f.setNextAnim(op.popEnterAnim);
+ mManager.showFragment(f);
+ break;
+ case OP_SHOW:
+ f.setNextAnim(op.popExitAnim);
+ mManager.hideFragment(f);
+ break;
+ case OP_DETACH:
+ f.setNextAnim(op.popEnterAnim);
+ mManager.attachFragment(f);
+ break;
+ case OP_ATTACH:
+ f.setNextAnim(op.popExitAnim);
+ mManager.detachFragment(f);
+ break;
+ case OP_SET_PRIMARY_NAV:
+ mManager.setPrimaryNavigationFragment(null);
+ break;
+ case OP_UNSET_PRIMARY_NAV:
+ mManager.setPrimaryNavigationFragment(f);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
+ }
+ if (!mReorderingAllowed && op.cmd != OP_REMOVE && f != null) {
+ mManager.moveFragmentToExpectedState(f);
+ }
+ }
+ if (!mReorderingAllowed && moveToState) {
+ mManager.moveToState(mManager.mCurState, true);
+ }
+ }
+
+ /**
+ * Expands all meta-ops into their more primitive equivalents. This must be called prior to
+ * {@link #executeOps()} or any other call that operations on mOps for forward navigation.
+ * It should not be called for pop/reverse navigation operations.
+ *
+ * <p>Removes all OP_REPLACE ops and replaces them with the proper add and remove
+ * operations that are equivalent to the replace.</p>
+ *
+ * <p>Adds OP_UNSET_PRIMARY_NAV ops to match OP_SET_PRIMARY_NAV, OP_REMOVE and OP_DETACH
+ * ops so that we can restore the old primary nav fragment later. Since callers call this
+ * method in a loop before running ops from several transactions at once, the caller should
+ * pass the return value from this method as the oldPrimaryNav parameter for the next call.
+ * The first call in such a loop should pass the value of
+ * {@link FragmentManager#getPrimaryNavigationFragment()}.</p>
+ *
+ * @param added Initialized to the fragments that are in the mManager.mAdded, this
+ * will be modified to contain the fragments that will be in mAdded
+ * after the execution ({@link #executeOps()}.
+ * @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of
+ * this set of ops
+ * @return the new oldPrimaryNav fragment after this record's ops would be run
+ */
+ @SuppressWarnings("ReferenceEquality")
+ Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {
+ for (int opNum = 0; opNum < mOps.size(); opNum++) {
+ final Op op = mOps.get(opNum);
+ switch (op.cmd) {
+ case OP_ADD:
+ case OP_ATTACH:
+ added.add(op.fragment);
+ break;
+ case OP_REMOVE:
+ case OP_DETACH: {
+ added.remove(op.fragment);
+ if (op.fragment == oldPrimaryNav) {
+ mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, op.fragment));
+ opNum++;
+ oldPrimaryNav = null;
+ }
+ }
+ break;
+ case OP_REPLACE: {
+ final Fragment f = op.fragment;
+ final int containerId = f.mContainerId;
+ boolean alreadyAdded = false;
+ for (int i = added.size() - 1; i >= 0; i--) {
+ final Fragment old = added.get(i);
+ if (old.mContainerId == containerId) {
+ if (old == f) {
+ alreadyAdded = true;
+ } else {
+ // This is duplicated from above since we only make
+ // a single pass for expanding ops. Unset any outgoing primary nav.
+ if (old == oldPrimaryNav) {
+ mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old));
+ opNum++;
+ oldPrimaryNav = null;
+ }
+ final Op removeOp = new Op(OP_REMOVE, old);
+ removeOp.enterAnim = op.enterAnim;
+ removeOp.popEnterAnim = op.popEnterAnim;
+ removeOp.exitAnim = op.exitAnim;
+ removeOp.popExitAnim = op.popExitAnim;
+ mOps.add(opNum, removeOp);
+ added.remove(old);
+ opNum++;
+ }
+ }
+ }
+ if (alreadyAdded) {
+ mOps.remove(opNum);
+ opNum--;
+ } else {
+ op.cmd = OP_ADD;
+ added.add(f);
+ }
+ }
+ break;
+ case OP_SET_PRIMARY_NAV: {
+ // It's ok if this is null, that means we will restore to no active
+ // primary navigation fragment on a pop.
+ mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, oldPrimaryNav));
+ opNum++;
+ // Will be set by the OP_SET_PRIMARY_NAV we inserted before when run
+ oldPrimaryNav = op.fragment;
+ }
+ break;
+ }
+ }
+ return oldPrimaryNav;
+ }
+
+ /**
+ * Removes fragments that are added or removed during a pop operation.
+ *
+ * @param added Initialized to the fragments that are in the mManager.mAdded, this
+ * will be modified to contain the fragments that will be in mAdded
+ * after the execution ({@link #executeOps()}.
+ */
+ void trackAddedFragmentsInPop(ArrayList<Fragment> added) {
+ for (int opNum = 0; opNum < mOps.size(); opNum++) {
+ final Op op = mOps.get(opNum);
+ switch (op.cmd) {
+ case OP_ADD:
+ case OP_ATTACH:
+ added.remove(op.fragment);
+ break;
+ case OP_REMOVE:
+ case OP_DETACH:
+ added.add(op.fragment);
+ break;
+ }
+ }
+ }
+
+ boolean isPostponed() {
+ for (int opNum = 0; opNum < mOps.size(); opNum++) {
+ final Op op = mOps.get(opNum);
+ if (isFragmentPostponed(op)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void setOnStartPostponedListener(Fragment.OnStartEnterTransitionListener listener) {
+ for (int opNum = 0; opNum < mOps.size(); opNum++) {
+ final Op op = mOps.get(opNum);
+ if (isFragmentPostponed(op)) {
+ op.fragment.setOnStartEnterTransitionListener(listener);
+ }
+ }
+ }
+
+ private static boolean isFragmentPostponed(Op op) {
+ final Fragment fragment = op.fragment;
+ return fragment != null && fragment.mAdded && fragment.mView != null && !fragment.mDetached
+ && !fragment.mHidden && fragment.isPostponed();
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getTransition() {
+ return mTransition;
+ }
+
+ public int getTransitionStyle() {
+ return mTransitionStyle;
+ }
+
+ public boolean isEmpty() {
+ return mOps.isEmpty();
+ }
+}
diff --git a/android/app/BroadcastOptions.java b/android/app/BroadcastOptions.java
new file mode 100644
index 00000000..b6cff385
--- /dev/null
+++ b/android/app/BroadcastOptions.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.Build;
+import android.os.Bundle;
+
+/**
+ * Helper class for building an options Bundle that can be used with
+ * {@link android.content.Context#sendBroadcast(android.content.Intent)
+ * Context.sendBroadcast(Intent)} and related methods.
+ * {@hide}
+ */
+@SystemApi
+public class BroadcastOptions {
+ private long mTemporaryAppWhitelistDuration;
+ private int mMinManifestReceiverApiLevel = 0;
+ private int mMaxManifestReceiverApiLevel = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+ /**
+ * How long to temporarily put an app on the power whitelist when executing this broadcast
+ * to it.
+ */
+ static final String KEY_TEMPORARY_APP_WHITELIST_DURATION
+ = "android:broadcast.temporaryAppWhitelistDuration";
+
+ /**
+ * Corresponds to {@link #setMinManifestReceiverApiLevel}.
+ */
+ static final String KEY_MIN_MANIFEST_RECEIVER_API_LEVEL
+ = "android:broadcast.minManifestReceiverApiLevel";
+
+ /**
+ * Corresponds to {@link #setMaxManifestReceiverApiLevel}.
+ */
+ static final String KEY_MAX_MANIFEST_RECEIVER_API_LEVEL
+ = "android:broadcast.maxManifestReceiverApiLevel";
+
+ public static BroadcastOptions makeBasic() {
+ BroadcastOptions opts = new BroadcastOptions();
+ return opts;
+ }
+
+ private BroadcastOptions() {
+ }
+
+ /** @hide */
+ public BroadcastOptions(Bundle opts) {
+ mTemporaryAppWhitelistDuration = opts.getLong(KEY_TEMPORARY_APP_WHITELIST_DURATION);
+ mMinManifestReceiverApiLevel = opts.getInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, 0);
+ mMaxManifestReceiverApiLevel = opts.getInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL,
+ Build.VERSION_CODES.CUR_DEVELOPMENT);
+ }
+
+ /**
+ * Set a duration for which the system should temporary place an application on the
+ * power whitelist when this broadcast is being delivered to it.
+ * @param duration The duration in milliseconds; 0 means to not place on whitelist.
+ */
+ @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST)
+ public void setTemporaryAppWhitelistDuration(long duration) {
+ mTemporaryAppWhitelistDuration = duration;
+ }
+
+ /**
+ * Return {@link #setTemporaryAppWhitelistDuration}.
+ * @hide
+ */
+ public long getTemporaryAppWhitelistDuration() {
+ return mTemporaryAppWhitelistDuration;
+ }
+
+ /**
+ * Set the minimum target API level of receivers of the broadcast. If an application
+ * is targeting an API level less than this, the broadcast will not be delivered to
+ * them. This only applies to receivers declared in the app's AndroidManifest.xml.
+ * @hide
+ */
+ public void setMinManifestReceiverApiLevel(int apiLevel) {
+ mMinManifestReceiverApiLevel = apiLevel;
+ }
+
+ /**
+ * Return {@link #setMinManifestReceiverApiLevel}.
+ * @hide
+ */
+ public int getMinManifestReceiverApiLevel() {
+ return mMinManifestReceiverApiLevel;
+ }
+
+ /**
+ * Set the maximum target API level of receivers of the broadcast. If an application
+ * is targeting an API level greater than this, the broadcast will not be delivered to
+ * them. This only applies to receivers declared in the app's AndroidManifest.xml.
+ * @hide
+ */
+ public void setMaxManifestReceiverApiLevel(int apiLevel) {
+ mMaxManifestReceiverApiLevel = apiLevel;
+ }
+
+ /**
+ * Return {@link #setMaxManifestReceiverApiLevel}.
+ * @hide
+ */
+ public int getMaxManifestReceiverApiLevel() {
+ return mMaxManifestReceiverApiLevel;
+ }
+
+ /**
+ * Returns the created options as a Bundle, which can be passed to
+ * {@link android.content.Context#sendBroadcast(android.content.Intent)
+ * Context.sendBroadcast(Intent)} and related methods.
+ * Note that the returned Bundle is still owned by the BroadcastOptions
+ * object; you must not modify it, but can supply it to the sendBroadcast
+ * methods that take an options Bundle.
+ */
+ public Bundle toBundle() {
+ Bundle b = new Bundle();
+ if (mTemporaryAppWhitelistDuration > 0) {
+ b.putLong(KEY_TEMPORARY_APP_WHITELIST_DURATION, mTemporaryAppWhitelistDuration);
+ }
+ if (mMinManifestReceiverApiLevel != 0) {
+ b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
+ }
+ if (mMaxManifestReceiverApiLevel != Build.VERSION_CODES.CUR_DEVELOPMENT) {
+ b.putInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL, mMaxManifestReceiverApiLevel);
+ }
+ return b.isEmpty() ? null : b;
+ }
+}
diff --git a/android/app/ContentProviderHolder.java b/android/app/ContentProviderHolder.java
new file mode 100644
index 00000000..f9998f4a
--- /dev/null
+++ b/android/app/ContentProviderHolder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ContentProviderNative;
+import android.content.IContentProvider;
+import android.content.pm.ProviderInfo;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information you can retrieve about a particular application.
+ *
+ * @hide
+ */
+public class ContentProviderHolder implements Parcelable {
+ public final ProviderInfo info;
+ public IContentProvider provider;
+ public IBinder connection;
+ public boolean noReleaseNeeded;
+
+ public ContentProviderHolder(ProviderInfo _info) {
+ info = _info;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ info.writeToParcel(dest, 0);
+ if (provider != null) {
+ dest.writeStrongBinder(provider.asBinder());
+ } else {
+ dest.writeStrongBinder(null);
+ }
+ dest.writeStrongBinder(connection);
+ dest.writeInt(noReleaseNeeded ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<ContentProviderHolder> CREATOR
+ = new Parcelable.Creator<ContentProviderHolder>() {
+ @Override
+ public ContentProviderHolder createFromParcel(Parcel source) {
+ return new ContentProviderHolder(source);
+ }
+
+ @Override
+ public ContentProviderHolder[] newArray(int size) {
+ return new ContentProviderHolder[size];
+ }
+ };
+
+ private ContentProviderHolder(Parcel source) {
+ info = ProviderInfo.CREATOR.createFromParcel(source);
+ provider = ContentProviderNative.asInterface(
+ source.readStrongBinder());
+ connection = source.readStrongBinder();
+ noReleaseNeeded = source.readInt() != 0;
+ }
+} \ No newline at end of file
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
new file mode 100644
index 00000000..c48be770
--- /dev/null
+++ b/android/app/ContextImpl.java
@@ -0,0 +1,2536 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.IContentProvider;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.ReceiverCallNotAllowedException;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetManager;
+import android.content.res.CompatResources;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.IStorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructStat;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import libcore.io.Memory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Objects;
+
+class ReceiverRestrictedContext extends ContextWrapper {
+ ReceiverRestrictedContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return registerReceiver(receiver, filter, null, null);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ if (receiver == null) {
+ // Allow retrieving current sticky broadcast; this is safe since we
+ // aren't actually registering a receiver.
+ return super.registerReceiver(null, filter, broadcastPermission, scheduler);
+ } else {
+ throw new ReceiverCallNotAllowedException(
+ "BroadcastReceiver components are not allowed to register to receive intents");
+ }
+ }
+
+ @Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ if (receiver == null) {
+ // Allow retrieving current sticky broadcast; this is safe since we
+ // aren't actually registering a receiver.
+ return super.registerReceiverAsUser(null, user, filter, broadcastPermission, scheduler);
+ } else {
+ throw new ReceiverCallNotAllowedException(
+ "BroadcastReceiver components are not allowed to register to receive intents");
+ }
+ }
+
+ @Override
+ public boolean bindService(Intent service, ServiceConnection conn, int flags) {
+ throw new ReceiverCallNotAllowedException(
+ "BroadcastReceiver components are not allowed to bind to services");
+ }
+}
+
+/**
+ * Common implementation of Context API, which provides the base
+ * context object for Activity and other application components.
+ */
+class ContextImpl extends Context {
+ private final static String TAG = "ContextImpl";
+ private final static boolean DEBUG = false;
+
+ private static final String XATTR_INODE_CACHE = "user.inode_cache";
+ private static final String XATTR_INODE_CODE_CACHE = "user.inode_code_cache";
+
+ /**
+ * Map from package name, to preference name, to cached preferences.
+ */
+ @GuardedBy("ContextImpl.class")
+ private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
+
+ /**
+ * Map from preference name to generated path.
+ */
+ @GuardedBy("ContextImpl.class")
+ private ArrayMap<String, File> mSharedPrefsPaths;
+
+ final @NonNull ActivityThread mMainThread;
+ final @NonNull LoadedApk mPackageInfo;
+ private @Nullable ClassLoader mClassLoader;
+
+ private final @Nullable IBinder mActivityToken;
+
+ private final @Nullable UserHandle mUser;
+
+ private final ApplicationContentResolver mContentResolver;
+
+ private final String mBasePackageName;
+ private final String mOpPackageName;
+
+ private final @NonNull ResourcesManager mResourcesManager;
+ private @NonNull Resources mResources;
+ private @Nullable Display mDisplay; // may be null if default display
+
+ private final int mFlags;
+
+ private Context mOuterContext;
+ private int mThemeResource = 0;
+ private Resources.Theme mTheme = null;
+ private PackageManager mPackageManager;
+ private Context mReceiverRestrictedContext = null;
+
+ // The name of the split this Context is representing. May be null.
+ private @Nullable String mSplitName = null;
+
+ private final Object mSync = new Object();
+
+ @GuardedBy("mSync")
+ private File mDatabasesDir;
+ @GuardedBy("mSync")
+ private File mPreferencesDir;
+ @GuardedBy("mSync")
+ private File mFilesDir;
+ @GuardedBy("mSync")
+ private File mNoBackupFilesDir;
+ @GuardedBy("mSync")
+ private File mCacheDir;
+ @GuardedBy("mSync")
+ private File mCodeCacheDir;
+
+ // The system service cache for the system services that are cached per-ContextImpl.
+ final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
+
+ static ContextImpl getImpl(Context context) {
+ Context nextContext;
+ while ((context instanceof ContextWrapper) &&
+ (nextContext=((ContextWrapper)context).getBaseContext()) != null) {
+ context = nextContext;
+ }
+ return (ContextImpl)context;
+ }
+
+ @Override
+ public AssetManager getAssets() {
+ return getResources().getAssets();
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ if (mPackageManager != null) {
+ return mPackageManager;
+ }
+
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm != null) {
+ // Doesn't matter if we make more than one instance.
+ return (mPackageManager = new ApplicationPackageManager(this, pm));
+ }
+
+ return null;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public Looper getMainLooper() {
+ return mMainThread.getLooper();
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return (mPackageInfo != null) ?
+ mPackageInfo.getApplication() : mMainThread.getApplication();
+ }
+
+ @Override
+ public void setTheme(int resId) {
+ synchronized (mSync) {
+ if (mThemeResource != resId) {
+ mThemeResource = resId;
+ initializeTheme();
+ }
+ }
+ }
+
+ @Override
+ public int getThemeResId() {
+ synchronized (mSync) {
+ return mThemeResource;
+ }
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ synchronized (mSync) {
+ if (mTheme != null) {
+ return mTheme;
+ }
+
+ mThemeResource = Resources.selectDefaultTheme(mThemeResource,
+ getOuterContext().getApplicationInfo().targetSdkVersion);
+ initializeTheme();
+
+ return mTheme;
+ }
+ }
+
+ private void initializeTheme() {
+ if (mTheme == null) {
+ mTheme = mResources.newTheme();
+ }
+ mTheme.applyStyle(mThemeResource, true);
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
+ }
+
+ @Override
+ public String getPackageName() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getPackageName();
+ }
+ // No mPackageInfo means this is a Context for the system itself,
+ // and this here is its name.
+ return "android";
+ }
+
+ /** @hide */
+ @Override
+ public String getBasePackageName() {
+ return mBasePackageName != null ? mBasePackageName : getPackageName();
+ }
+
+ /** @hide */
+ @Override
+ public String getOpPackageName() {
+ return mOpPackageName != null ? mOpPackageName : getBasePackageName();
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getApplicationInfo();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getResDir();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public String getPackageCodePath() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getAppDir();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ // At least one application in the world actually passes in a null
+ // name. This happened to work because when we generated the file name
+ // we would stringify it to "null.xml". Nice.
+ if (mPackageInfo.getApplicationInfo().targetSdkVersion <
+ Build.VERSION_CODES.KITKAT) {
+ if (name == null) {
+ name = "null";
+ }
+ }
+
+ File file;
+ synchronized (ContextImpl.class) {
+ if (mSharedPrefsPaths == null) {
+ mSharedPrefsPaths = new ArrayMap<>();
+ }
+ file = mSharedPrefsPaths.get(name);
+ if (file == null) {
+ file = getSharedPreferencesPath(name);
+ mSharedPrefsPaths.put(name, file);
+ }
+ }
+ return getSharedPreferences(file, mode);
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(File file, int mode) {
+ SharedPreferencesImpl sp;
+ synchronized (ContextImpl.class) {
+ final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
+ sp = cache.get(file);
+ if (sp == null) {
+ checkMode(mode);
+ if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
+ if (isCredentialProtectedStorage()
+ && !getSystemService(UserManager.class)
+ .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
+ throw new IllegalStateException("SharedPreferences in credential encrypted "
+ + "storage are not available until after user is unlocked");
+ }
+ }
+ sp = new SharedPreferencesImpl(file, mode);
+ cache.put(file, sp);
+ return sp;
+ }
+ }
+ if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
+ getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
+ // If somebody else (some other process) changed the prefs
+ // file behind our back, we reload it. This has been the
+ // historical (if undocumented) behavior.
+ sp.startReloadIfChangedUnexpectedly();
+ }
+ return sp;
+ }
+
+ private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
+ if (sSharedPrefsCache == null) {
+ sSharedPrefsCache = new ArrayMap<>();
+ }
+
+ final String packageName = getPackageName();
+ ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
+ if (packagePrefs == null) {
+ packagePrefs = new ArrayMap<>();
+ sSharedPrefsCache.put(packageName, packagePrefs);
+ }
+
+ return packagePrefs;
+ }
+
+ @Override
+ public void reloadSharedPreferences() {
+ // Build the list of all per-context impls (i.e. caches) we know about
+ ArrayList<SharedPreferencesImpl> spImpls = new ArrayList<>();
+ synchronized (ContextImpl.class) {
+ final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
+ for (int i = 0; i < cache.size(); i++) {
+ final SharedPreferencesImpl sp = cache.valueAt(i);
+ if (sp != null) {
+ spImpls.add(sp);
+ }
+ }
+ }
+
+ // Issue the reload outside the cache lock
+ for (int i = 0; i < spImpls.size(); i++) {
+ spImpls.get(i).startReloadIfChangedUnexpectedly();
+ }
+ }
+
+ /**
+ * Try our best to migrate all files from source to target that match
+ * requested prefix.
+ *
+ * @return the number of files moved, or -1 if there was trouble.
+ */
+ private static int moveFiles(File sourceDir, File targetDir, final String prefix) {
+ final File[] sourceFiles = FileUtils.listFilesOrEmpty(sourceDir, new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.startsWith(prefix);
+ }
+ });
+
+ int res = 0;
+ for (File sourceFile : sourceFiles) {
+ final File targetFile = new File(targetDir, sourceFile.getName());
+ Log.d(TAG, "Migrating " + sourceFile + " to " + targetFile);
+ try {
+ FileUtils.copyFileOrThrow(sourceFile, targetFile);
+ FileUtils.copyPermissions(sourceFile, targetFile);
+ if (!sourceFile.delete()) {
+ throw new IOException("Failed to clean up " + sourceFile);
+ }
+ if (res != -1) {
+ res++;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to migrate " + sourceFile + ": " + e);
+ res = -1;
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public boolean moveSharedPreferencesFrom(Context sourceContext, String name) {
+ synchronized (ContextImpl.class) {
+ final File source = sourceContext.getSharedPreferencesPath(name);
+ final File target = getSharedPreferencesPath(name);
+
+ final int res = moveFiles(source.getParentFile(), target.getParentFile(),
+ source.getName());
+ if (res > 0) {
+ // We moved at least one file, so evict any in-memory caches for
+ // either location
+ final ArrayMap<File, SharedPreferencesImpl> cache =
+ getSharedPreferencesCacheLocked();
+ cache.remove(source);
+ cache.remove(target);
+ }
+ return res != -1;
+ }
+ }
+
+ @Override
+ public boolean deleteSharedPreferences(String name) {
+ synchronized (ContextImpl.class) {
+ final File prefs = getSharedPreferencesPath(name);
+ final File prefsBackup = SharedPreferencesImpl.makeBackupFile(prefs);
+
+ // Evict any in-memory caches
+ final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
+ cache.remove(prefs);
+
+ prefs.delete();
+ prefsBackup.delete();
+
+ // We failed if files are still lingering
+ return !(prefs.exists() || prefsBackup.exists());
+ }
+ }
+
+ private File getPreferencesDir() {
+ synchronized (mSync) {
+ if (mPreferencesDir == null) {
+ mPreferencesDir = new File(getDataDir(), "shared_prefs");
+ }
+ return ensurePrivateDirExists(mPreferencesDir);
+ }
+ }
+
+ @Override
+ public FileInputStream openFileInput(String name)
+ throws FileNotFoundException {
+ File f = makeFilename(getFilesDir(), name);
+ return new FileInputStream(f);
+ }
+
+ @Override
+ public FileOutputStream openFileOutput(String name, int mode) throws FileNotFoundException {
+ checkMode(mode);
+ final boolean append = (mode&MODE_APPEND) != 0;
+ File f = makeFilename(getFilesDir(), name);
+ try {
+ FileOutputStream fos = new FileOutputStream(f, append);
+ setFilePermissionsFromMode(f.getPath(), mode, 0);
+ return fos;
+ } catch (FileNotFoundException e) {
+ }
+
+ File parent = f.getParentFile();
+ parent.mkdir();
+ FileUtils.setPermissions(
+ parent.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ FileOutputStream fos = new FileOutputStream(f, append);
+ setFilePermissionsFromMode(f.getPath(), mode, 0);
+ return fos;
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ File f = makeFilename(getFilesDir(), name);
+ return f.delete();
+ }
+
+ /**
+ * Common-path handling of app data dir creation
+ */
+ private static File ensurePrivateDirExists(File file) {
+ return ensurePrivateDirExists(file, 0771, -1, null);
+ }
+
+ private static File ensurePrivateCacheDirExists(File file, String xattr) {
+ final int gid = UserHandle.getCacheAppGid(Process.myUid());
+ return ensurePrivateDirExists(file, 02771, gid, xattr);
+ }
+
+ private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
+ if (!file.exists()) {
+ final String path = file.getAbsolutePath();
+ try {
+ Os.mkdir(path, mode);
+ Os.chmod(path, mode);
+ if (gid != -1) {
+ Os.chown(path, -1, gid);
+ }
+ } catch (ErrnoException e) {
+ if (e.errno == OsConstants.EEXIST) {
+ // We must have raced with someone; that's okay
+ } else {
+ Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
+ }
+ }
+
+ if (xattr != null) {
+ try {
+ final StructStat stat = Os.stat(file.getAbsolutePath());
+ final byte[] value = new byte[8];
+ Memory.pokeLong(value, 0, stat.st_ino, ByteOrder.nativeOrder());
+ Os.setxattr(file.getParentFile().getAbsolutePath(), xattr, value, 0);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "Failed to update " + xattr + ": " + e.getMessage());
+ }
+ }
+ }
+ return file;
+ }
+
+ @Override
+ public File getFilesDir() {
+ synchronized (mSync) {
+ if (mFilesDir == null) {
+ mFilesDir = new File(getDataDir(), "files");
+ }
+ return ensurePrivateDirExists(mFilesDir);
+ }
+ }
+
+ @Override
+ public File getNoBackupFilesDir() {
+ synchronized (mSync) {
+ if (mNoBackupFilesDir == null) {
+ mNoBackupFilesDir = new File(getDataDir(), "no_backup");
+ }
+ return ensurePrivateDirExists(mNoBackupFilesDir);
+ }
+ }
+
+ @Override
+ public File getExternalFilesDir(String type) {
+ // Operates on primary external storage
+ final File[] dirs = getExternalFilesDirs(type);
+ return (dirs != null && dirs.length > 0) ? dirs[0] : null;
+ }
+
+ @Override
+ public File[] getExternalFilesDirs(String type) {
+ synchronized (mSync) {
+ File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
+ if (type != null) {
+ dirs = Environment.buildPaths(dirs, type);
+ }
+ return ensureExternalDirsExistOrFilter(dirs);
+ }
+ }
+
+ @Override
+ public File getObbDir() {
+ // Operates on primary external storage
+ final File[] dirs = getObbDirs();
+ return (dirs != null && dirs.length > 0) ? dirs[0] : null;
+ }
+
+ @Override
+ public File[] getObbDirs() {
+ synchronized (mSync) {
+ File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName());
+ return ensureExternalDirsExistOrFilter(dirs);
+ }
+ }
+
+ @Override
+ public File getCacheDir() {
+ synchronized (mSync) {
+ if (mCacheDir == null) {
+ mCacheDir = new File(getDataDir(), "cache");
+ }
+ return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE);
+ }
+ }
+
+ @Override
+ public File getCodeCacheDir() {
+ synchronized (mSync) {
+ if (mCodeCacheDir == null) {
+ mCodeCacheDir = new File(getDataDir(), "code_cache");
+ }
+ return ensurePrivateCacheDirExists(mCodeCacheDir, XATTR_INODE_CODE_CACHE);
+ }
+ }
+
+ @Override
+ public File getExternalCacheDir() {
+ // Operates on primary external storage
+ final File[] dirs = getExternalCacheDirs();
+ return (dirs != null && dirs.length > 0) ? dirs[0] : null;
+ }
+
+ @Override
+ public File[] getExternalCacheDirs() {
+ synchronized (mSync) {
+ File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
+ return ensureExternalDirsExistOrFilter(dirs);
+ }
+ }
+
+ @Override
+ public File[] getExternalMediaDirs() {
+ synchronized (mSync) {
+ File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName());
+ return ensureExternalDirsExistOrFilter(dirs);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Nullable
+ @Override
+ public File getPreloadsFileCache() {
+ return Environment.getDataPreloadsFileCacheDirectory(getPackageName());
+ }
+
+ @Override
+ public File getFileStreamPath(String name) {
+ return makeFilename(getFilesDir(), name);
+ }
+
+ @Override
+ public File getSharedPreferencesPath(String name) {
+ return makeFilename(getPreferencesDir(), name + ".xml");
+ }
+
+ @Override
+ public String[] fileList() {
+ return FileUtils.listOrEmpty(getFilesDir());
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
+ return openOrCreateDatabase(name, mode, factory, null);
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ checkMode(mode);
+ File f = getDatabasePath(name);
+ int flags = SQLiteDatabase.CREATE_IF_NECESSARY;
+ if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) {
+ flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING;
+ }
+ if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) {
+ flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS;
+ }
+ SQLiteDatabase db = SQLiteDatabase.openDatabase(f.getPath(), factory, flags, errorHandler);
+ setFilePermissionsFromMode(f.getPath(), mode, 0);
+ return db;
+ }
+
+ @Override
+ public boolean moveDatabaseFrom(Context sourceContext, String name) {
+ synchronized (ContextImpl.class) {
+ final File source = sourceContext.getDatabasePath(name);
+ final File target = getDatabasePath(name);
+ return moveFiles(source.getParentFile(), target.getParentFile(),
+ source.getName()) != -1;
+ }
+ }
+
+ @Override
+ public boolean deleteDatabase(String name) {
+ try {
+ File f = getDatabasePath(name);
+ return SQLiteDatabase.deleteDatabase(f);
+ } catch (Exception e) {
+ }
+ return false;
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ File dir;
+ File f;
+
+ if (name.charAt(0) == File.separatorChar) {
+ String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar));
+ dir = new File(dirPath);
+ name = name.substring(name.lastIndexOf(File.separatorChar));
+ f = new File(dir, name);
+
+ if (!dir.isDirectory() && dir.mkdir()) {
+ FileUtils.setPermissions(dir.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ }
+ } else {
+ dir = getDatabasesDir();
+ f = makeFilename(dir, name);
+ }
+
+ return f;
+ }
+
+ @Override
+ public String[] databaseList() {
+ return FileUtils.listOrEmpty(getDatabasesDir());
+ }
+
+ private File getDatabasesDir() {
+ synchronized (mSync) {
+ if (mDatabasesDir == null) {
+ if ("android".equals(getPackageName())) {
+ mDatabasesDir = new File("/data/system");
+ } else {
+ mDatabasesDir = new File(getDataDir(), "databases");
+ }
+ }
+ return ensurePrivateDirExists(mDatabasesDir);
+ }
+ }
+
+ @Override
+ @Deprecated
+ public Drawable getWallpaper() {
+ return getWallpaperManager().getDrawable();
+ }
+
+ @Override
+ @Deprecated
+ public Drawable peekWallpaper() {
+ return getWallpaperManager().peekDrawable();
+ }
+
+ @Override
+ @Deprecated
+ public int getWallpaperDesiredMinimumWidth() {
+ return getWallpaperManager().getDesiredMinimumWidth();
+ }
+
+ @Override
+ @Deprecated
+ public int getWallpaperDesiredMinimumHeight() {
+ return getWallpaperManager().getDesiredMinimumHeight();
+ }
+
+ @Override
+ @Deprecated
+ public void setWallpaper(Bitmap bitmap) throws IOException {
+ getWallpaperManager().setBitmap(bitmap);
+ }
+
+ @Override
+ @Deprecated
+ public void setWallpaper(InputStream data) throws IOException {
+ getWallpaperManager().setStream(data);
+ }
+
+ @Override
+ @Deprecated
+ public void clearWallpaper() throws IOException {
+ getWallpaperManager().clear();
+ }
+
+ private WallpaperManager getWallpaperManager() {
+ return getSystemService(WallpaperManager.class);
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ warnIfCallingFromSystemProcess();
+ startActivity(intent, null);
+ }
+
+ /** @hide */
+ @Override
+ public void startActivityAsUser(Intent intent, UserHandle user) {
+ startActivityAsUser(intent, null, user);
+ }
+
+ @Override
+ public void startActivity(Intent intent, Bundle options) {
+ warnIfCallingFromSystemProcess();
+
+ // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
+ // generally not allowed, except if the caller specifies the task id the activity should
+ // be launched in.
+ if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
+ && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
+ throw new AndroidRuntimeException(
+ "Calling startActivity() from outside of an Activity "
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ + " Is this really what you want?");
+ }
+ mMainThread.getInstrumentation().execStartActivity(
+ getOuterContext(), mMainThread.getApplicationThread(), null,
+ (Activity) null, intent, -1, options);
+ }
+
+ /** @hide */
+ @Override
+ public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
+ try {
+ ActivityManager.getService().startActivityAsUser(
+ mMainThread.getApplicationThread(), getBasePackageName(), intent,
+ intent.resolveTypeIfNeeded(getContentResolver()),
+ null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, options,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void startActivities(Intent[] intents) {
+ warnIfCallingFromSystemProcess();
+ startActivities(intents, null);
+ }
+
+ /** @hide */
+ @Override
+ public void startActivitiesAsUser(Intent[] intents, Bundle options, UserHandle userHandle) {
+ if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
+ throw new AndroidRuntimeException(
+ "Calling startActivities() from outside of an Activity "
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
+ + " Is this really what you want?");
+ }
+ mMainThread.getInstrumentation().execStartActivitiesAsUser(
+ getOuterContext(), mMainThread.getApplicationThread(), null,
+ (Activity) null, intents, options, userHandle.getIdentifier());
+ }
+
+ @Override
+ public void startActivities(Intent[] intents, Bundle options) {
+ warnIfCallingFromSystemProcess();
+ if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
+ throw new AndroidRuntimeException(
+ "Calling startActivities() from outside of an Activity "
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
+ + " Is this really what you want?");
+ }
+ mMainThread.getInstrumentation().execStartActivities(
+ getOuterContext(), mMainThread.getApplicationThread(), null,
+ (Activity) null, intents, options);
+ }
+
+ @Override
+ public void startIntentSender(IntentSender intent,
+ Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)
+ throws IntentSender.SendIntentException {
+ startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags, null);
+ }
+
+ @Override
+ public void startIntentSender(IntentSender intent, Intent fillInIntent,
+ int flagsMask, int flagsValues, int extraFlags, Bundle options)
+ throws IntentSender.SendIntentException {
+ try {
+ String resolvedType = null;
+ if (fillInIntent != null) {
+ fillInIntent.migrateExtraStreamToClipData();
+ fillInIntent.prepareToLeaveProcess(this);
+ resolvedType = fillInIntent.resolveTypeIfNeeded(getContentResolver());
+ }
+ int result = ActivityManager.getService()
+ .startActivityIntentSender(mMainThread.getApplicationThread(),
+ intent != null ? intent.getTarget() : null,
+ intent != null ? intent.getWhitelistToken() : null,
+ fillInIntent, resolvedType, null, null,
+ 0, flagsMask, flagsValues, options);
+ if (result == ActivityManager.START_CANCELED) {
+ throw new IntentSender.SendIntentException();
+ }
+ Instrumentation.checkStartActivityResult(result, null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, false,
+ getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, String receiverPermission) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ null, false, false, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ null, false, false, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ options, false, false, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, String receiverPermission, int appOp) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, appOp, null, false, false,
+ getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent, String receiverPermission) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ null, true, false, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ sendOrderedBroadcast(intent, receiverPermission, AppOpsManager.OP_NONE,
+ resultReceiver, scheduler, initialCode, initialData, initialExtras, null);
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, Bundle options, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ sendOrderedBroadcast(intent, receiverPermission, AppOpsManager.OP_NONE,
+ resultReceiver, scheduler, initialCode, initialData, initialExtras, options);
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ sendOrderedBroadcast(intent, receiverPermission, appOp,
+ resultReceiver, scheduler, initialCode, initialData, initialExtras, null);
+ }
+
+ void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras, Bundle options) {
+ warnIfCallingFromSystemProcess();
+ IIntentReceiver rd = null;
+ if (resultReceiver != null) {
+ if (mPackageInfo != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler,
+ mMainThread.getInstrumentation(), false);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new LoadedApk.ReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, rd,
+ initialCode, initialData, initialExtras, receiverPermissions, appOp,
+ options, true, false, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(mMainThread.getApplicationThread(),
+ intent, resolvedType, null, Activity.RESULT_OK, null, null, null,
+ AppOpsManager.OP_NONE, null, false, false, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission) {
+ sendBroadcastAsUser(intent, user, receiverPermission, AppOpsManager.OP_NONE);
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+ Bundle options) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ options, false, false, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, appOp, null, false, false,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
+ int initialCode, String initialData, Bundle initialExtras) {
+ sendOrderedBroadcastAsUser(intent, user, receiverPermission, AppOpsManager.OP_NONE,
+ null, resultReceiver, scheduler, initialCode, initialData, initialExtras);
+ }
+
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
+ sendOrderedBroadcastAsUser(intent, user, receiverPermission, appOp,
+ null, resultReceiver, scheduler, initialCode, initialData, initialExtras);
+ }
+
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, Bundle options, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
+ IIntentReceiver rd = null;
+ if (resultReceiver != null) {
+ if (mPackageInfo != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler,
+ mMainThread.getInstrumentation(), false);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new LoadedApk.ReceiverDispatcher(resultReceiver, getOuterContext(),
+ scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ String[] receiverPermissions = receiverPermission == null ? null
+ : new String[] {receiverPermission};
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, rd,
+ initialCode, initialData, initialExtras, receiverPermissions,
+ appOp, options, true, false, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void sendStickyBroadcast(Intent intent) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, true,
+ getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void sendStickyOrderedBroadcast(Intent intent,
+ BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ warnIfCallingFromSystemProcess();
+ IIntentReceiver rd = null;
+ if (resultReceiver != null) {
+ if (mPackageInfo != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler,
+ mMainThread.getInstrumentation(), false);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new LoadedApk.ReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, rd,
+ initialCode, initialData, initialExtras, null,
+ AppOpsManager.OP_NONE, null, true, true, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void removeStickyBroadcast(Intent intent) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ if (resolvedType != null) {
+ intent = new Intent(intent);
+ intent.setDataAndType(intent.getData(), resolvedType);
+ }
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().unbroadcastIntent(
+ mMainThread.getApplicationThread(), intent, getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, true,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void sendStickyBroadcastAsUser(Intent intent, UserHandle user, Bundle options) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, options, false, true,
+ user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void sendStickyOrderedBroadcastAsUser(Intent intent,
+ UserHandle user, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ IIntentReceiver rd = null;
+ if (resultReceiver != null) {
+ if (mPackageInfo != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler,
+ mMainThread.getInstrumentation(), false);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new LoadedApk.ReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, rd,
+ initialCode, initialData, initialExtras, null,
+ AppOpsManager.OP_NONE, null, true, true, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void removeStickyBroadcastAsUser(Intent intent, UserHandle user) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ if (resolvedType != null) {
+ intent = new Intent(intent);
+ intent.setDataAndType(intent.getData(), resolvedType);
+ }
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().unbroadcastIntent(
+ mMainThread.getApplicationThread(), intent, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return registerReceiver(receiver, filter, null, null);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ int flags) {
+ return registerReceiver(receiver, filter, null, null, flags);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ return registerReceiverInternal(receiver, getUserId(),
+ filter, broadcastPermission, scheduler, getOuterContext(), 0);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler, int flags) {
+ return registerReceiverInternal(receiver, getUserId(),
+ filter, broadcastPermission, scheduler, getOuterContext(), flags);
+ }
+
+ @Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ return registerReceiverInternal(receiver, user.getIdentifier(),
+ filter, broadcastPermission, scheduler, getOuterContext(), 0);
+ }
+
+ private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
+ IntentFilter filter, String broadcastPermission,
+ Handler scheduler, Context context, int flags) {
+ IIntentReceiver rd = null;
+ if (receiver != null) {
+ if (mPackageInfo != null && context != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ receiver, context, scheduler,
+ mMainThread.getInstrumentation(), true);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new LoadedApk.ReceiverDispatcher(
+ receiver, context, scheduler, null, true).getIIntentReceiver();
+ }
+ }
+ try {
+ final Intent intent = ActivityManager.getService().registerReceiver(
+ mMainThread.getApplicationThread(), mBasePackageName, rd, filter,
+ broadcastPermission, userId, flags);
+ if (intent != null) {
+ intent.setExtrasClassLoader(getClassLoader());
+ intent.prepareToEnterProcess();
+ }
+ return intent;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+ if (mPackageInfo != null) {
+ IIntentReceiver rd = mPackageInfo.forgetReceiverDispatcher(
+ getOuterContext(), receiver);
+ try {
+ ActivityManager.getService().unregisterReceiver(rd);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ }
+
+ private void validateServiceIntent(Intent service) {
+ if (service.getComponent() == null && service.getPackage() == null) {
+ if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
+ IllegalArgumentException ex = new IllegalArgumentException(
+ "Service Intent must be explicit: " + service);
+ throw ex;
+ } else {
+ Log.w(TAG, "Implicit intents with startService are not safe: " + service
+ + " " + Debug.getCallers(2, 3));
+ }
+ }
+ }
+
+ @Override
+ public ComponentName startService(Intent service) {
+ warnIfCallingFromSystemProcess();
+ return startServiceCommon(service, false, mUser);
+ }
+
+ @Override
+ public ComponentName startForegroundService(Intent service) {
+ warnIfCallingFromSystemProcess();
+ return startServiceCommon(service, true, mUser);
+ }
+
+ @Override
+ public boolean stopService(Intent service) {
+ warnIfCallingFromSystemProcess();
+ return stopServiceCommon(service, mUser);
+ }
+
+ @Override
+ public ComponentName startServiceAsUser(Intent service, UserHandle user) {
+ return startServiceCommon(service, false, user);
+ }
+
+ @Override
+ public ComponentName startForegroundServiceAsUser(Intent service, UserHandle user) {
+ return startServiceCommon(service, true, user);
+ }
+
+ private ComponentName startServiceCommon(Intent service, boolean requireForeground,
+ UserHandle user) {
+ try {
+ validateServiceIntent(service);
+ service.prepareToLeaveProcess(this);
+ ComponentName cn = ActivityManager.getService().startService(
+ mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
+ getContentResolver()), requireForeground,
+ getOpPackageName(), user.getIdentifier());
+ if (cn != null) {
+ if (cn.getPackageName().equals("!")) {
+ throw new SecurityException(
+ "Not allowed to start service " + service
+ + " without permission " + cn.getClassName());
+ } else if (cn.getPackageName().equals("!!")) {
+ throw new SecurityException(
+ "Unable to start service " + service
+ + ": " + cn.getClassName());
+ } else if (cn.getPackageName().equals("?")) {
+ throw new IllegalStateException(
+ "Not allowed to start service " + service + ": " + cn.getClassName());
+ }
+ }
+ return cn;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean stopServiceAsUser(Intent service, UserHandle user) {
+ return stopServiceCommon(service, user);
+ }
+
+ private boolean stopServiceCommon(Intent service, UserHandle user) {
+ try {
+ validateServiceIntent(service);
+ service.prepareToLeaveProcess(this);
+ int res = ActivityManager.getService().stopService(
+ mMainThread.getApplicationThread(), service,
+ service.resolveTypeIfNeeded(getContentResolver()), user.getIdentifier());
+ if (res < 0) {
+ throw new SecurityException(
+ "Not allowed to stop service " + service);
+ }
+ return res != 0;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean bindService(Intent service, ServiceConnection conn,
+ int flags) {
+ warnIfCallingFromSystemProcess();
+ return bindServiceCommon(service, conn, flags, mMainThread.getHandler(),
+ Process.myUserHandle());
+ }
+
+ /** @hide */
+ @Override
+ public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+ UserHandle user) {
+ return bindServiceCommon(service, conn, flags, mMainThread.getHandler(), user);
+ }
+
+ /** @hide */
+ @Override
+ public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+ Handler handler, UserHandle user) {
+ if (handler == null) {
+ throw new IllegalArgumentException("handler must not be null.");
+ }
+ return bindServiceCommon(service, conn, flags, handler, user);
+ }
+
+ /** @hide */
+ @Override
+ public IServiceConnection getServiceDispatcher(ServiceConnection conn, Handler handler,
+ int flags) {
+ return mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
+ }
+
+ /** @hide */
+ @Override
+ public IApplicationThread getIApplicationThread() {
+ return mMainThread.getApplicationThread();
+ }
+
+ /** @hide */
+ @Override
+ public Handler getMainThreadHandler() {
+ return mMainThread.getHandler();
+ }
+
+ private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags, Handler
+ handler, UserHandle user) {
+ // Keep this in sync with DevicePolicyManager.bindDeviceAdminServiceAsUser.
+ IServiceConnection sd;
+ if (conn == null) {
+ throw new IllegalArgumentException("connection is null");
+ }
+ if (mPackageInfo != null) {
+ sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ validateServiceIntent(service);
+ try {
+ IBinder token = getActivityToken();
+ if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null
+ && mPackageInfo.getApplicationInfo().targetSdkVersion
+ < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ flags |= BIND_WAIVE_PRIORITY;
+ }
+ service.prepareToLeaveProcess(this);
+ int res = ActivityManager.getService().bindService(
+ mMainThread.getApplicationThread(), getActivityToken(), service,
+ service.resolveTypeIfNeeded(getContentResolver()),
+ sd, flags, getOpPackageName(), user.getIdentifier());
+ if (res < 0) {
+ throw new SecurityException(
+ "Not allowed to bind to service " + service);
+ }
+ return res != 0;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ if (conn == null) {
+ throw new IllegalArgumentException("connection is null");
+ }
+ if (mPackageInfo != null) {
+ IServiceConnection sd = mPackageInfo.forgetServiceDispatcher(
+ getOuterContext(), conn);
+ try {
+ ActivityManager.getService().unbindService(sd);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ }
+
+ @Override
+ public boolean startInstrumentation(ComponentName className,
+ String profileFile, Bundle arguments) {
+ try {
+ if (arguments != null) {
+ arguments.setAllowFds(false);
+ }
+ return ActivityManager.getService().startInstrumentation(
+ className, profileFile, 0, arguments, null, null, getUserId(),
+ null /* ABI override */);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ return SystemServiceRegistry.getSystemService(this, name);
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ return SystemServiceRegistry.getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public int checkPermission(String permission, int pid, int uid) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ final IActivityManager am = ActivityManager.getService();
+ if (am == null) {
+ // Well this is super awkward; we somehow don't have an active
+ // ActivityManager instance. If we're testing a root or system
+ // UID, then they totally have whatever permission this is.
+ final int appId = UserHandle.getAppId(uid);
+ if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
+ Slog.w(TAG, "Missing ActivityManager; assuming " + uid + " holds " + permission);
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ try {
+ return am.checkPermission(permission, pid, uid);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public int checkPermission(String permission, int pid, int uid, IBinder callerToken) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ try {
+ return ActivityManager.getService().checkPermissionWithToken(
+ permission, pid, uid, callerToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int checkCallingPermission(String permission) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ int pid = Binder.getCallingPid();
+ if (pid != Process.myPid()) {
+ return checkPermission(permission, pid, Binder.getCallingUid());
+ }
+ return PackageManager.PERMISSION_DENIED;
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ return checkPermission(permission, Binder.getCallingPid(),
+ Binder.getCallingUid());
+ }
+
+ @Override
+ public int checkSelfPermission(String permission) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ return checkPermission(permission, Process.myPid(), Process.myUid());
+ }
+
+ private void enforce(
+ String permission, int resultOfCheck,
+ boolean selfToo, int uid, String message) {
+ if (resultOfCheck != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ (message != null ? (message + ": ") : "") +
+ (selfToo
+ ? "Neither user " + uid + " nor current process has "
+ : "uid " + uid + " does not have ") +
+ permission +
+ ".");
+ }
+ }
+
+ @Override
+ public void enforcePermission(
+ String permission, int pid, int uid, String message) {
+ enforce(permission,
+ checkPermission(permission, pid, uid),
+ false,
+ uid,
+ message);
+ }
+
+ @Override
+ public void enforceCallingPermission(String permission, String message) {
+ enforce(permission,
+ checkCallingPermission(permission),
+ false,
+ Binder.getCallingUid(),
+ message);
+ }
+
+ @Override
+ public void enforceCallingOrSelfPermission(
+ String permission, String message) {
+ enforce(permission,
+ checkCallingOrSelfPermission(permission),
+ true,
+ Binder.getCallingUid(),
+ message);
+ }
+
+ @Override
+ public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
+ try {
+ ActivityManager.getService().grantUriPermission(
+ mMainThread.getApplicationThread(), toPackage,
+ ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void revokeUriPermission(Uri uri, int modeFlags) {
+ try {
+ ActivityManager.getService().revokeUriPermission(
+ mMainThread.getApplicationThread(), null,
+ ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+ try {
+ ActivityManager.getService().revokeUriPermission(
+ mMainThread.getApplicationThread(), targetPackage,
+ ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+ try {
+ return ActivityManager.getService().checkUriPermission(
+ ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags,
+ resolveUserId(uri), null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags, IBinder callerToken) {
+ try {
+ return ActivityManager.getService().checkUriPermission(
+ ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags,
+ resolveUserId(uri), callerToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private int resolveUserId(Uri uri) {
+ return ContentProvider.getUserIdFromUri(uri, getUserId());
+ }
+
+ @Override
+ public int checkCallingUriPermission(Uri uri, int modeFlags) {
+ int pid = Binder.getCallingPid();
+ if (pid != Process.myPid()) {
+ return checkUriPermission(uri, pid,
+ Binder.getCallingUid(), modeFlags);
+ }
+ return PackageManager.PERMISSION_DENIED;
+ }
+
+ @Override
+ public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) {
+ return checkUriPermission(uri, Binder.getCallingPid(),
+ Binder.getCallingUid(), modeFlags);
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, String readPermission,
+ String writePermission, int pid, int uid, int modeFlags) {
+ if (DEBUG) {
+ Log.i("foo", "checkUriPermission: uri=" + uri + "readPermission="
+ + readPermission + " writePermission=" + writePermission
+ + " pid=" + pid + " uid=" + uid + " mode" + modeFlags);
+ }
+ if ((modeFlags&Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
+ if (readPermission == null
+ || checkPermission(readPermission, pid, uid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ if ((modeFlags&Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+ if (writePermission == null
+ || checkPermission(writePermission, pid, uid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return uri != null ? checkUriPermission(uri, pid, uid, modeFlags)
+ : PackageManager.PERMISSION_DENIED;
+ }
+
+ private String uriModeFlagToString(int uriModeFlags) {
+ StringBuilder builder = new StringBuilder();
+ if ((uriModeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
+ builder.append("read and ");
+ }
+ if ((uriModeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+ builder.append("write and ");
+ }
+ if ((uriModeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0) {
+ builder.append("persistable and ");
+ }
+ if ((uriModeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) != 0) {
+ builder.append("prefix and ");
+ }
+
+ if (builder.length() > 5) {
+ builder.setLength(builder.length() - 5);
+ return builder.toString();
+ } else {
+ throw new IllegalArgumentException("Unknown permission mode flags: " + uriModeFlags);
+ }
+ }
+
+ private void enforceForUri(
+ int modeFlags, int resultOfCheck, boolean selfToo,
+ int uid, Uri uri, String message) {
+ if (resultOfCheck != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ (message != null ? (message + ": ") : "") +
+ (selfToo
+ ? "Neither user " + uid + " nor current process has "
+ : "User " + uid + " does not have ") +
+ uriModeFlagToString(modeFlags) +
+ " permission on " +
+ uri +
+ ".");
+ }
+ }
+
+ @Override
+ public void enforceUriPermission(
+ Uri uri, int pid, int uid, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags, checkUriPermission(uri, pid, uid, modeFlags),
+ false, uid, uri, message);
+ }
+
+ @Override
+ public void enforceCallingUriPermission(
+ Uri uri, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags, checkCallingUriPermission(uri, modeFlags),
+ false,
+ Binder.getCallingUid(), uri, message);
+ }
+
+ @Override
+ public void enforceCallingOrSelfUriPermission(
+ Uri uri, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags,
+ checkCallingOrSelfUriPermission(uri, modeFlags), true,
+ Binder.getCallingUid(), uri, message);
+ }
+
+ @Override
+ public void enforceUriPermission(
+ Uri uri, String readPermission, String writePermission,
+ int pid, int uid, int modeFlags, String message) {
+ enforceForUri(modeFlags,
+ checkUriPermission(
+ uri, readPermission, writePermission, pid, uid,
+ modeFlags),
+ false,
+ uid,
+ uri,
+ message);
+ }
+
+ /**
+ * Logs a warning if the system process directly called a method such as
+ * {@link #startService(Intent)} instead of {@link #startServiceAsUser(Intent, UserHandle)}.
+ * The "AsUser" variants allow us to properly enforce the user's restrictions.
+ */
+ private void warnIfCallingFromSystemProcess() {
+ if (Process.myUid() == Process.SYSTEM_UID) {
+ Slog.w(TAG, "Calling a method in the system process without a qualified user: "
+ + Debug.getCallers(5));
+ }
+ }
+
+ private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
+ int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
+ final String[] splitResDirs;
+ final ClassLoader classLoader;
+ try {
+ splitResDirs = pi.getSplitPaths(splitName);
+ classLoader = pi.getSplitClassLoader(splitName);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ return ResourcesManager.getInstance().getResources(activityToken,
+ pi.getResDir(),
+ splitResDirs,
+ pi.getOverlayDirs(),
+ pi.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ overrideConfig,
+ compatInfo,
+ classLoader);
+ }
+
+ @Override
+ public Context createApplicationContext(ApplicationInfo application, int flags)
+ throws NameNotFoundException {
+ LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
+ flags | CONTEXT_REGISTER_PACKAGE);
+ if (pi != null) {
+ ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken,
+ new UserHandle(UserHandle.getUserId(application.uid)), flags, null);
+
+ final int displayId = mDisplay != null
+ ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
+
+ c.setResources(createResources(mActivityToken, pi, null, displayId, null,
+ getDisplayAdjustments(displayId).getCompatibilityInfo()));
+ if (c.mResources != null) {
+ return c;
+ }
+ }
+
+ throw new PackageManager.NameNotFoundException(
+ "Application package " + application.packageName + " not found");
+ }
+
+ @Override
+ public Context createPackageContext(String packageName, int flags)
+ throws NameNotFoundException {
+ return createPackageContextAsUser(packageName, flags,
+ mUser != null ? mUser : Process.myUserHandle());
+ }
+
+ @Override
+ public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
+ throws NameNotFoundException {
+ if (packageName.equals("system") || packageName.equals("android")) {
+ // The system resources are loaded in every application, so we can safely copy
+ // the context without reloading Resources.
+ return new ContextImpl(this, mMainThread, mPackageInfo, null, mActivityToken, user,
+ flags, null);
+ }
+
+ LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
+ flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
+ if (pi != null) {
+ ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, user,
+ flags, null);
+
+ final int displayId = mDisplay != null
+ ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
+
+ c.setResources(createResources(mActivityToken, pi, null, displayId, null,
+ getDisplayAdjustments(displayId).getCompatibilityInfo()));
+ if (c.mResources != null) {
+ return c;
+ }
+ }
+
+ // Should be a better exception.
+ throw new PackageManager.NameNotFoundException(
+ "Application package " + packageName + " not found");
+ }
+
+ @Override
+ public Context createContextForSplit(String splitName) throws NameNotFoundException {
+ if (!mPackageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ // All Splits are always loaded.
+ return this;
+ }
+
+ final ClassLoader classLoader = mPackageInfo.getSplitClassLoader(splitName);
+ final String[] paths = mPackageInfo.getSplitPaths(splitName);
+
+ final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, splitName,
+ mActivityToken, mUser, mFlags, classLoader);
+
+ final int displayId = mDisplay != null
+ ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
+
+ context.setResources(ResourcesManager.getInstance().getResources(
+ mActivityToken,
+ mPackageInfo.getResDir(),
+ paths,
+ mPackageInfo.getOverlayDirs(),
+ mPackageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ null,
+ mPackageInfo.getCompatibilityInfo(),
+ classLoader));
+ return context;
+ }
+
+ @Override
+ public Context createConfigurationContext(Configuration overrideConfiguration) {
+ if (overrideConfiguration == null) {
+ throw new IllegalArgumentException("overrideConfiguration must not be null");
+ }
+
+ ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName,
+ mActivityToken, mUser, mFlags, mClassLoader);
+
+ final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
+ context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId,
+ overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo()));
+ return context;
+ }
+
+ @Override
+ public Context createDisplayContext(Display display) {
+ if (display == null) {
+ throw new IllegalArgumentException("display must not be null");
+ }
+
+ ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName,
+ mActivityToken, mUser, mFlags, mClassLoader);
+
+ final int displayId = display.getDisplayId();
+ context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId,
+ null, getDisplayAdjustments(displayId).getCompatibilityInfo()));
+ context.mDisplay = display;
+ return context;
+ }
+
+ @Override
+ public Context createDeviceProtectedStorageContext() {
+ final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE)
+ | Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
+ return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser,
+ flags, mClassLoader);
+ }
+
+ @Override
+ public Context createCredentialProtectedStorageContext() {
+ final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE)
+ | Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
+ return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser,
+ flags, mClassLoader);
+ }
+
+ @Override
+ public boolean isRestricted() {
+ return (mFlags & Context.CONTEXT_RESTRICTED) != 0;
+ }
+
+ @Override
+ public boolean isDeviceProtectedStorage() {
+ return (mFlags & Context.CONTEXT_DEVICE_PROTECTED_STORAGE) != 0;
+ }
+
+ @Override
+ public boolean isCredentialProtectedStorage() {
+ return (mFlags & Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE) != 0;
+ }
+
+ @Override
+ public boolean canLoadUnsafeResources() {
+ if (getPackageName().equals(getOpPackageName())) {
+ return true;
+ }
+ return (mFlags & Context.CONTEXT_IGNORE_SECURITY) != 0;
+ }
+
+ @Override
+ public Display getDisplay() {
+ if (mDisplay == null) {
+ return mResourcesManager.getAdjustedDisplay(Display.DEFAULT_DISPLAY,
+ mResources);
+ }
+
+ return mDisplay;
+ }
+
+ @Override
+ public void updateDisplay(int displayId) {
+ mDisplay = mResourcesManager.getAdjustedDisplay(displayId, mResources);
+ }
+
+ @Override
+ public DisplayAdjustments getDisplayAdjustments(int displayId) {
+ return mResources.getDisplayAdjustments();
+ }
+
+ @Override
+ public File getDataDir() {
+ if (mPackageInfo != null) {
+ File res = null;
+ if (isCredentialProtectedStorage()) {
+ res = mPackageInfo.getCredentialProtectedDataDirFile();
+ } else if (isDeviceProtectedStorage()) {
+ res = mPackageInfo.getDeviceProtectedDataDirFile();
+ } else {
+ res = mPackageInfo.getDataDirFile();
+ }
+
+ if (res != null) {
+ if (!res.exists() && android.os.Process.myUid() == android.os.Process.SYSTEM_UID) {
+ Log.wtf(TAG, "Data directory doesn't exist for package " + getPackageName(),
+ new Throwable());
+ }
+ return res;
+ } else {
+ throw new RuntimeException(
+ "No data directory found for package " + getPackageName());
+ }
+ } else {
+ throw new RuntimeException(
+ "No package details found for package " + getPackageName());
+ }
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ checkMode(mode);
+ name = "app_" + name;
+ File file = makeFilename(getDataDir(), name);
+ if (!file.exists()) {
+ file.mkdir();
+ setFilePermissionsFromMode(file.getPath(), mode,
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH);
+ }
+ return file;
+ }
+
+ /** {@hide} */
+ @Override
+ public int getUserId() {
+ return mUser.getIdentifier();
+ }
+
+ static ContextImpl createSystemContext(ActivityThread mainThread) {
+ LoadedApk packageInfo = new LoadedApk(mainThread);
+ ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
+ null);
+ context.setResources(packageInfo.getResources());
+ context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
+ context.mResourcesManager.getDisplayMetrics());
+ return context;
+ }
+
+ /**
+ * System Context to be used for UI. This Context has resources that can be themed.
+ * Make sure that the created system UI context shares the same LoadedApk as the system context.
+ */
+ static ContextImpl createSystemUiContext(ContextImpl systemContext) {
+ final LoadedApk packageInfo = systemContext.mPackageInfo;
+ ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo, null,
+ null, null, 0, null);
+ context.setResources(createResources(null, packageInfo, null, Display.DEFAULT_DISPLAY, null,
+ packageInfo.getCompatibilityInfo()));
+ return context;
+ }
+
+ static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
+ if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
+ ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
+ null);
+ context.setResources(packageInfo.getResources());
+ return context;
+ }
+
+ static ContextImpl createActivityContext(ActivityThread mainThread,
+ LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
+ Configuration overrideConfiguration) {
+ if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
+
+ String[] splitDirs = packageInfo.getSplitResDirs();
+ ClassLoader classLoader = packageInfo.getClassLoader();
+
+ if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
+ try {
+ classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
+ splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
+ } catch (NameNotFoundException e) {
+ // Nothing above us can handle a NameNotFoundException, better crash.
+ throw new RuntimeException(e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
+ activityToken, null, 0, classLoader);
+
+ // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
+ displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;
+
+ final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
+ ? packageInfo.getCompatibilityInfo()
+ : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
+
+ final ResourcesManager resourcesManager = ResourcesManager.getInstance();
+
+ // Create the base resources for which all configuration contexts for this Activity
+ // will be rebased upon.
+ context.setResources(resourcesManager.createBaseActivityResources(activityToken,
+ packageInfo.getResDir(),
+ splitDirs,
+ packageInfo.getOverlayDirs(),
+ packageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ overrideConfiguration,
+ compatInfo,
+ classLoader));
+ context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
+ context.getResources());
+ return context;
+ }
+
+ private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread,
+ @NonNull LoadedApk packageInfo, @Nullable String splitName,
+ @Nullable IBinder activityToken, @Nullable UserHandle user, int flags,
+ @Nullable ClassLoader classLoader) {
+ mOuterContext = this;
+
+ // If creator didn't specify which storage to use, use the default
+ // location for application.
+ if ((flags & (Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE
+ | Context.CONTEXT_DEVICE_PROTECTED_STORAGE)) == 0) {
+ final File dataDir = packageInfo.getDataDirFile();
+ if (Objects.equals(dataDir, packageInfo.getCredentialProtectedDataDirFile())) {
+ flags |= Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
+ } else if (Objects.equals(dataDir, packageInfo.getDeviceProtectedDataDirFile())) {
+ flags |= Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
+ }
+ }
+
+ mMainThread = mainThread;
+ mActivityToken = activityToken;
+ mFlags = flags;
+
+ if (user == null) {
+ user = Process.myUserHandle();
+ }
+ mUser = user;
+
+ mPackageInfo = packageInfo;
+ mSplitName = splitName;
+ mClassLoader = classLoader;
+ mResourcesManager = ResourcesManager.getInstance();
+
+ if (container != null) {
+ mBasePackageName = container.mBasePackageName;
+ mOpPackageName = container.mOpPackageName;
+ setResources(container.mResources);
+ mDisplay = container.mDisplay;
+ } else {
+ mBasePackageName = packageInfo.mPackageName;
+ ApplicationInfo ainfo = packageInfo.getApplicationInfo();
+ if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
+ // Special case: system components allow themselves to be loaded in to other
+ // processes. For purposes of app ops, we must then consider the context as
+ // belonging to the package of this process, not the system itself, otherwise
+ // the package+uid verifications in app ops will fail.
+ mOpPackageName = ActivityThread.currentPackageName();
+ } else {
+ mOpPackageName = mBasePackageName;
+ }
+ }
+
+ mContentResolver = new ApplicationContentResolver(this, mainThread, user);
+ }
+
+ void setResources(Resources r) {
+ if (r instanceof CompatResources) {
+ ((CompatResources) r).setContext(this);
+ }
+ mResources = r;
+ }
+
+ void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
+ mPackageInfo.installSystemApplicationInfo(info, classLoader);
+ }
+
+ final void scheduleFinalCleanup(String who, String what) {
+ mMainThread.scheduleContextCleanup(this, who, what);
+ }
+
+ final void performFinalCleanup(String who, String what) {
+ //Log.i(TAG, "Cleanup up context: " + this);
+ mPackageInfo.removeContextRegistrations(getOuterContext(), who, what);
+ }
+
+ final Context getReceiverRestrictedContext() {
+ if (mReceiverRestrictedContext != null) {
+ return mReceiverRestrictedContext;
+ }
+ return mReceiverRestrictedContext = new ReceiverRestrictedContext(getOuterContext());
+ }
+
+ final void setOuterContext(Context context) {
+ mOuterContext = context;
+ }
+
+ final Context getOuterContext() {
+ return mOuterContext;
+ }
+
+ @Override
+ public IBinder getActivityToken() {
+ return mActivityToken;
+ }
+
+ private void checkMode(int mode) {
+ if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
+ if ((mode & MODE_WORLD_READABLE) != 0) {
+ throw new SecurityException("MODE_WORLD_READABLE no longer supported");
+ }
+ if ((mode & MODE_WORLD_WRITEABLE) != 0) {
+ throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ static void setFilePermissionsFromMode(String name, int mode,
+ int extraPermissions) {
+ int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
+ |FileUtils.S_IRGRP|FileUtils.S_IWGRP
+ |extraPermissions;
+ if ((mode&MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+ if ((mode&MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+ if (DEBUG) {
+ Log.i(TAG, "File " + name + ": mode=0x" + Integer.toHexString(mode)
+ + ", perms=0x" + Integer.toHexString(perms));
+ }
+ FileUtils.setPermissions(name, perms, -1, -1);
+ }
+
+ private File makeFilename(File base, String name) {
+ if (name.indexOf(File.separatorChar) < 0) {
+ return new File(base, name);
+ }
+ throw new IllegalArgumentException(
+ "File " + name + " contains a path separator");
+ }
+
+ /**
+ * Ensure that given directories exist, trying to create them if missing. If
+ * unable to create, they are filtered by replacing with {@code null}.
+ */
+ private File[] ensureExternalDirsExistOrFilter(File[] dirs) {
+ File[] result = new File[dirs.length];
+ for (int i = 0; i < dirs.length; i++) {
+ File dir = dirs[i];
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ // recheck existence in case of cross-process race
+ if (!dir.exists()) {
+ // Failing to mkdir() may be okay, since we might not have
+ // enough permissions; ask vold to create on our behalf.
+ final IStorageManager storageManager = IStorageManager.Stub.asInterface(
+ ServiceManager.getService("mount"));
+ try {
+ final int res = storageManager.mkdirs(
+ getPackageName(), dir.getAbsolutePath());
+ if (res != 0) {
+ Log.w(TAG, "Failed to ensure " + dir + ": " + res);
+ dir = null;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to ensure " + dir + ": " + e);
+ dir = null;
+ }
+ }
+ }
+ }
+ result[i] = dir;
+ }
+ return result;
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+
+ private static final class ApplicationContentResolver extends ContentResolver {
+ private final ActivityThread mMainThread;
+ private final UserHandle mUser;
+
+ public ApplicationContentResolver(
+ Context context, ActivityThread mainThread, UserHandle user) {
+ super(context);
+ mMainThread = Preconditions.checkNotNull(mainThread);
+ mUser = Preconditions.checkNotNull(user);
+ }
+
+ @Override
+ protected IContentProvider acquireProvider(Context context, String auth) {
+ return mMainThread.acquireProvider(context,
+ ContentProvider.getAuthorityWithoutUserId(auth),
+ resolveUserIdFromAuthority(auth), true);
+ }
+
+ @Override
+ protected IContentProvider acquireExistingProvider(Context context, String auth) {
+ return mMainThread.acquireExistingProvider(context,
+ ContentProvider.getAuthorityWithoutUserId(auth),
+ resolveUserIdFromAuthority(auth), true);
+ }
+
+ @Override
+ public boolean releaseProvider(IContentProvider provider) {
+ return mMainThread.releaseProvider(provider, true);
+ }
+
+ @Override
+ protected IContentProvider acquireUnstableProvider(Context c, String auth) {
+ return mMainThread.acquireProvider(c,
+ ContentProvider.getAuthorityWithoutUserId(auth),
+ resolveUserIdFromAuthority(auth), false);
+ }
+
+ @Override
+ public boolean releaseUnstableProvider(IContentProvider icp) {
+ return mMainThread.releaseProvider(icp, false);
+ }
+
+ @Override
+ public void unstableProviderDied(IContentProvider icp) {
+ mMainThread.handleUnstableProviderDied(icp.asBinder(), true);
+ }
+
+ @Override
+ public void appNotRespondingViaProvider(IContentProvider icp) {
+ mMainThread.appNotRespondingViaProvider(icp.asBinder());
+ }
+
+ /** @hide */
+ protected int resolveUserIdFromAuthority(String auth) {
+ return ContentProvider.getUserIdFromAuthority(auth, mUser.getIdentifier());
+ }
+ }
+}
diff --git a/android/app/DatePickerDialog.java b/android/app/DatePickerDialog.java
new file mode 100644
index 00000000..bd55a06c
--- /dev/null
+++ b/android/app/DatePickerDialog.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.DatePicker.OnDateChangedListener;
+import android.widget.DatePicker.ValidationCallback;
+
+import com.android.internal.R;
+
+import java.util.Calendar;
+
+/**
+ * A simple dialog containing an {@link android.widget.DatePicker}.
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide.
+ */
+public class DatePickerDialog extends AlertDialog implements OnClickListener,
+ OnDateChangedListener {
+ private static final String YEAR = "year";
+ private static final String MONTH = "month";
+ private static final String DAY = "day";
+
+ private final DatePicker mDatePicker;
+
+ private OnDateSetListener mDateSetListener;
+
+ /**
+ * Creates a new date picker dialog for the current date using the parent
+ * context's default date picker dialog theme.
+ *
+ * @param context the parent context
+ */
+ public DatePickerDialog(@NonNull Context context) {
+ this(context, 0, null, Calendar.getInstance(), -1, -1, -1);
+ }
+
+ /**
+ * Creates a new date picker dialog for the current date.
+ *
+ * @param context the parent context
+ * @param themeResId the resource ID of the theme against which to inflate
+ * this dialog, or {@code 0} to use the parent
+ * {@code context}'s default alert dialog theme
+ */
+ public DatePickerDialog(@NonNull Context context, @StyleRes int themeResId) {
+ this(context, themeResId, null, Calendar.getInstance(), -1, -1, -1);
+ }
+
+ /**
+ * Creates a new date picker dialog for the specified date using the parent
+ * context's default date picker dialog theme.
+ *
+ * @param context the parent context
+ * @param listener the listener to call when the user sets the date
+ * @param year the initially selected year
+ * @param month the initially selected month (0-11 for compatibility with
+ * {@link Calendar#MONTH})
+ * @param dayOfMonth the initially selected day of month (1-31, depending
+ * on month)
+ */
+ public DatePickerDialog(@NonNull Context context, @Nullable OnDateSetListener listener,
+ int year, int month, int dayOfMonth) {
+ this(context, 0, listener, null, year, month, dayOfMonth);
+ }
+
+ /**
+ * Creates a new date picker dialog for the specified date.
+ *
+ * @param context the parent context
+ * @param themeResId the resource ID of the theme against which to inflate
+ * this dialog, or {@code 0} to use the parent
+ * {@code context}'s default alert dialog theme
+ * @param listener the listener to call when the user sets the date
+ * @param year the initially selected year
+ * @param monthOfYear the initially selected month of the year (0-11 for
+ * compatibility with {@link Calendar#MONTH})
+ * @param dayOfMonth the initially selected day of month (1-31, depending
+ * on month)
+ */
+ public DatePickerDialog(@NonNull Context context, @StyleRes int themeResId,
+ @Nullable OnDateSetListener listener, int year, int monthOfYear, int dayOfMonth) {
+ this(context, themeResId, listener, null, year, monthOfYear, dayOfMonth);
+ }
+
+ private DatePickerDialog(@NonNull Context context, @StyleRes int themeResId,
+ @Nullable OnDateSetListener listener, @Nullable Calendar calendar, int year,
+ int monthOfYear, int dayOfMonth) {
+ super(context, resolveDialogTheme(context, themeResId));
+
+ final Context themeContext = getContext();
+ final LayoutInflater inflater = LayoutInflater.from(themeContext);
+ final View view = inflater.inflate(R.layout.date_picker_dialog, null);
+ setView(view);
+
+ setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this);
+ setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this);
+ setButtonPanelLayoutHint(LAYOUT_HINT_SIDE);
+
+ if (calendar != null) {
+ year = calendar.get(Calendar.YEAR);
+ monthOfYear = calendar.get(Calendar.MONTH);
+ dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
+ }
+
+ mDatePicker = (DatePicker) view.findViewById(R.id.datePicker);
+ mDatePicker.init(year, monthOfYear, dayOfMonth, this);
+ mDatePicker.setValidationCallback(mValidationCallback);
+
+ mDateSetListener = listener;
+ }
+
+ static @StyleRes int resolveDialogTheme(@NonNull Context context, @StyleRes int themeResId) {
+ if (themeResId == 0) {
+ final TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.datePickerDialogTheme, outValue, true);
+ return outValue.resourceId;
+ } else {
+ return themeResId;
+ }
+ }
+
+ @Override
+ public void onDateChanged(@NonNull DatePicker view, int year, int month, int dayOfMonth) {
+ mDatePicker.init(year, month, dayOfMonth, this);
+ }
+
+ /**
+ * Sets the listener to call when the user sets the date.
+ *
+ * @param listener the listener to call when the user sets the date
+ */
+ public void setOnDateSetListener(@Nullable OnDateSetListener listener) {
+ mDateSetListener = listener;
+ }
+
+ @Override
+ public void onClick(@NonNull DialogInterface dialog, int which) {
+ switch (which) {
+ case BUTTON_POSITIVE:
+ if (mDateSetListener != null) {
+ // Clearing focus forces the dialog to commit any pending
+ // changes, e.g. typed text in a NumberPicker.
+ mDatePicker.clearFocus();
+ mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(),
+ mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
+ }
+ break;
+ case BUTTON_NEGATIVE:
+ cancel();
+ break;
+ }
+ }
+
+ /**
+ * Returns the {@link DatePicker} contained in this dialog.
+ *
+ * @return the date picker
+ */
+ @NonNull
+ public DatePicker getDatePicker() {
+ return mDatePicker;
+ }
+
+ /**
+ * Sets the current date.
+ *
+ * @param year the year
+ * @param month the month (0-11 for compatibility with
+ * {@link Calendar#MONTH})
+ * @param dayOfMonth the day of month (1-31, depending on month)
+ */
+ public void updateDate(int year, int month, int dayOfMonth) {
+ mDatePicker.updateDate(year, month, dayOfMonth);
+ }
+
+ @Override
+ public Bundle onSaveInstanceState() {
+ final Bundle state = super.onSaveInstanceState();
+ state.putInt(YEAR, mDatePicker.getYear());
+ state.putInt(MONTH, mDatePicker.getMonth());
+ state.putInt(DAY, mDatePicker.getDayOfMonth());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ final int year = savedInstanceState.getInt(YEAR);
+ final int month = savedInstanceState.getInt(MONTH);
+ final int day = savedInstanceState.getInt(DAY);
+ mDatePicker.init(year, month, day, this);
+ }
+
+ private final ValidationCallback mValidationCallback = new ValidationCallback() {
+ @Override
+ public void onValidationChanged(boolean valid) {
+ final Button positive = getButton(BUTTON_POSITIVE);
+ if (positive != null) {
+ positive.setEnabled(valid);
+ }
+ }
+ };
+
+ /**
+ * The listener used to indicate the user has finished selecting a date.
+ */
+ public interface OnDateSetListener {
+ /**
+ * @param view the picker associated with the dialog
+ * @param year the selected year
+ * @param month the selected month (0-11 for compatibility with
+ * {@link Calendar#MONTH})
+ * @param dayOfMonth th selected day of the month (1-31, depending on
+ * month)
+ */
+ void onDateSet(DatePicker view, int year, int month, int dayOfMonth);
+ }
+}
diff --git a/android/app/DexLoadReporter.java b/android/app/DexLoadReporter.java
new file mode 100644
index 00000000..f99d1a8e
--- /dev/null
+++ b/android/app/DexLoadReporter.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.FileUtils;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.system.ErrnoException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import dalvik.system.BaseDexClassLoader;
+import dalvik.system.VMRuntime;
+
+import libcore.io.Libcore;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A dex load reporter which will notify package manager of any dex file loaded
+ * with {@code BaseDexClassLoader}.
+ * The goals are:
+ * 1) discover secondary dex files so that they can be optimized during the
+ * idle maintenance job.
+ * 2) determine whether or not a dex file is used by an app which does not
+ * own it (in order to select the optimal compilation method).
+ * @hide
+ */
+/*package*/ class DexLoadReporter implements BaseDexClassLoader.Reporter {
+ private static final String TAG = "DexLoadReporter";
+
+ private static final DexLoadReporter INSTANCE = new DexLoadReporter();
+
+ private static final boolean DEBUG = false;
+
+ // We must guard the access to the list of data directories because
+ // we might have concurrent accesses. Apps might load dex files while
+ // new data dirs are registered (due to creation of LoadedApks via
+ // create createApplicationContext).
+ @GuardedBy("mDataDirs")
+ private final Set<String> mDataDirs;
+
+ private DexLoadReporter() {
+ mDataDirs = new HashSet<>();
+ }
+
+ /*package*/ static DexLoadReporter getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Register an application data directory with the reporter.
+ * The data directories are used to determine if a dex file is secondary dex or not.
+ * Note that this method may be called multiple times for the same app, registering
+ * different data directories. This may happen when apps share the same user id
+ * ({@code android:sharedUserId}). For example, if app1 and app2 share the same user
+ * id, and app1 loads app2 apk, then both data directories will be registered.
+ */
+ /*package*/ void registerAppDataDir(String packageName, String dataDir) {
+ if (DEBUG) {
+ Slog.i(TAG, "Package " + packageName + " registering data dir: " + dataDir);
+ }
+ // TODO(calin): A few code paths imply that the data dir
+ // might be null. Investigate when that can happen.
+ if (dataDir != null) {
+ synchronized (mDataDirs) {
+ mDataDirs.add(dataDir);
+ }
+ }
+ }
+
+ @Override
+ public void report(List<BaseDexClassLoader> classLoadersChain, List<String> classPaths) {
+ if (classLoadersChain.size() != classPaths.size()) {
+ Slog.wtf(TAG, "Bad call to DexLoadReporter: argument size mismatch");
+ return;
+ }
+ if (classPaths.isEmpty()) {
+ Slog.wtf(TAG, "Bad call to DexLoadReporter: empty dex paths");
+ return;
+ }
+
+ // The first element of classPaths is the list of dex files that should be registered.
+ // The classpath is represented as a list of dex files separated by File.pathSeparator.
+ String[] dexPathsForRegistration = classPaths.get(0).split(File.pathSeparator);
+ if (dexPathsForRegistration.length == 0) {
+ // No dex files to register.
+ return;
+ }
+
+ // Notify the package manager about the dex loads unconditionally.
+ // The load might be for either a primary or secondary dex file.
+ notifyPackageManager(classLoadersChain, classPaths);
+ // Check for secondary dex files and register them for profiling if possible.
+ // Note that we only register the dex paths belonging to the first class loader.
+ registerSecondaryDexForProfiling(dexPathsForRegistration);
+ }
+
+ private void notifyPackageManager(List<BaseDexClassLoader> classLoadersChain,
+ List<String> classPaths) {
+ // Get the class loader names for the binder call.
+ List<String> classLoadersNames = new ArrayList<>(classPaths.size());
+ for (BaseDexClassLoader bdc : classLoadersChain) {
+ classLoadersNames.add(bdc.getClass().getName());
+ }
+ String packageName = ActivityThread.currentPackageName();
+ try {
+ ActivityThread.getPackageManager().notifyDexLoad(
+ packageName, classLoadersNames, classPaths,
+ VMRuntime.getRuntime().vmInstructionSet());
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Failed to notify PM about dex load for package " + packageName, re);
+ }
+ }
+
+ private void registerSecondaryDexForProfiling(String[] dexPaths) {
+ if (!SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
+ return;
+ }
+ // Make a copy of the current data directories so that we don't keep the lock
+ // while registering for profiling. The registration will perform I/O to
+ // check for or create the profile.
+ String[] dataDirs;
+ synchronized (mDataDirs) {
+ dataDirs = mDataDirs.toArray(new String[0]);
+ }
+ for (String dexPath : dexPaths) {
+ registerSecondaryDexForProfiling(dexPath, dataDirs);
+ }
+ }
+
+ private void registerSecondaryDexForProfiling(String dexPath, String[] dataDirs) {
+ if (!isSecondaryDexFile(dexPath, dataDirs)) {
+ // The dex path is not a secondary dex file. Nothing to do.
+ return;
+ }
+
+ File realDexPath;
+ try {
+ // Secondary dex profiles are stored in the oat directory, next to the real dex file
+ // and have the same name with 'cur.prof' appended. We use the realpath because that
+ // is what installd is using when processing the dex file.
+ // NOTE: Keep in sync with installd.
+ realDexPath = new File(Libcore.os.realpath(dexPath));
+ } catch (ErrnoException ex) {
+ Slog.e(TAG, "Failed to get the real path of secondary dex " + dexPath
+ + ":" + ex.getMessage());
+ // Do not continue with registration if we could not retrieve the real path.
+ return;
+ }
+
+ // NOTE: Keep this in sync with installd expectations.
+ File secondaryProfileDir = new File(realDexPath.getParent(), "oat");
+ File secondaryProfile = new File(secondaryProfileDir, realDexPath.getName() + ".cur.prof");
+
+ // Create the profile if not already there.
+ // Returns true if the file was created, false if the file already exists.
+ // or throws exceptions in case of errors.
+ if (!secondaryProfileDir.exists()) {
+ if (!secondaryProfileDir.mkdir()) {
+ Slog.e(TAG, "Could not create the profile directory: " + secondaryProfile);
+ // Do not continue with registration if we could not create the oat dir.
+ return;
+ }
+ }
+
+ try {
+ boolean created = secondaryProfile.createNewFile();
+ if (DEBUG && created) {
+ Slog.i(TAG, "Created profile for secondary dex: " + secondaryProfile);
+ }
+ } catch (IOException ex) {
+ Slog.e(TAG, "Failed to create profile for secondary dex " + dexPath
+ + ":" + ex.getMessage());
+ // Do not continue with registration if we could not create the profile files.
+ return;
+ }
+
+ // If we got here, the dex paths is a secondary dex and we were able to create the profile.
+ // Register the path to the runtime.
+ VMRuntime.registerAppInfo(secondaryProfile.getPath(), new String[] { dexPath });
+ }
+
+ // A dex file is a secondary dex file if it is in any of the registered app
+ // data directories.
+ private boolean isSecondaryDexFile(String dexPath, String[] dataDirs) {
+ for (String dataDir : dataDirs) {
+ if (FileUtils.contains(dataDir, dexPath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/android/app/Dialog.java b/android/app/Dialog.java
new file mode 100644
index 00000000..b162cb16
--- /dev/null
+++ b/android/app/Dialog.java
@@ -0,0 +1,1376 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.internal.R;
+import com.android.internal.app.WindowDecorActionBar;
+import com.android.internal.policy.PhoneWindow;
+
+import android.annotation.CallSuper;
+import android.annotation.DrawableRes;
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.pm.ApplicationInfo;
+import android.content.res.ResourceId;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SearchEvent;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Base class for Dialogs.
+ *
+ * <p>Note: Activities provide a facility to manage the creation, saving and
+ * restoring of dialogs. See {@link Activity#onCreateDialog(int)},
+ * {@link Activity#onPrepareDialog(int, Dialog)},
+ * {@link Activity#showDialog(int)}, and {@link Activity#dismissDialog(int)}. If
+ * these methods are used, {@link #getOwnerActivity()} will return the Activity
+ * that managed this dialog.
+ *
+ * <p>Often you will want to have a Dialog display on top of the current
+ * input method, because there is no reason for it to accept text. You can
+ * do this by setting the {@link WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM} window flag (assuming
+ * your Dialog takes input focus, as it the default) with the following code:
+ *
+ * <pre>
+ * getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);</pre>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating dialogs, read the
+ * <a href="{@docRoot}guide/topics/ui/dialogs.html">Dialogs</a> developer guide.</p>
+ * </div>
+ */
+public class Dialog implements DialogInterface, Window.Callback,
+ KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
+ private static final String TAG = "Dialog";
+ private Activity mOwnerActivity;
+
+ private final WindowManager mWindowManager;
+
+ final Context mContext;
+ final Window mWindow;
+
+ View mDecor;
+
+ private ActionBar mActionBar;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected boolean mCancelable = true;
+
+ private String mCancelAndDismissTaken;
+ private Message mCancelMessage;
+ private Message mDismissMessage;
+ private Message mShowMessage;
+
+ private OnKeyListener mOnKeyListener;
+
+ private boolean mCreated = false;
+ private boolean mShowing = false;
+ private boolean mCanceled = false;
+
+ private final Handler mHandler = new Handler();
+
+ private static final int DISMISS = 0x43;
+ private static final int CANCEL = 0x44;
+ private static final int SHOW = 0x45;
+
+ private final Handler mListenersHandler;
+
+ private SearchEvent mSearchEvent;
+
+ private ActionMode mActionMode;
+
+ private int mActionModeTypeStarting = ActionMode.TYPE_PRIMARY;
+
+ private final Runnable mDismissAction = this::dismissDialog;
+
+ /**
+ * Creates a dialog window that uses the default dialog theme.
+ * <p>
+ * The supplied {@code context} is used to obtain the window manager and
+ * base theme used to present the dialog.
+ *
+ * @param context the context in which the dialog should run
+ * @see android.R.styleable#Theme_dialogTheme
+ */
+ public Dialog(@NonNull Context context) {
+ this(context, 0, true);
+ }
+
+ /**
+ * Creates a dialog window that uses a custom dialog style.
+ * <p>
+ * The supplied {@code context} is used to obtain the window manager and
+ * base theme used to present the dialog.
+ * <p>
+ * The supplied {@code theme} is applied on top of the context's theme. See
+ * <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
+ * Style and Theme Resources</a> for more information about defining and
+ * using styles.
+ *
+ * @param context the context in which the dialog should run
+ * @param themeResId a style resource describing the theme to use for the
+ * window, or {@code 0} to use the default dialog theme
+ */
+ public Dialog(@NonNull Context context, @StyleRes int themeResId) {
+ this(context, themeResId, true);
+ }
+
+ Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
+ if (createContextThemeWrapper) {
+ if (themeResId == ResourceId.ID_NULL) {
+ final TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
+ themeResId = outValue.resourceId;
+ }
+ mContext = new ContextThemeWrapper(context, themeResId);
+ } else {
+ mContext = context;
+ }
+
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+ final Window w = new PhoneWindow(mContext);
+ mWindow = w;
+ w.setCallback(this);
+ w.setOnWindowDismissedCallback(this);
+ w.setOnWindowSwipeDismissedCallback(() -> {
+ if (mCancelable) {
+ cancel();
+ }
+ });
+ w.setWindowManager(mWindowManager, null, null);
+ w.setGravity(Gravity.CENTER);
+
+ mListenersHandler = new ListenersHandler(this);
+ }
+
+ /**
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ protected Dialog(@NonNull Context context, boolean cancelable,
+ @Nullable Message cancelCallback) {
+ this(context);
+ mCancelable = cancelable;
+ updateWindowForCancelable();
+ mCancelMessage = cancelCallback;
+ }
+
+ protected Dialog(@NonNull Context context, boolean cancelable,
+ @Nullable OnCancelListener cancelListener) {
+ this(context);
+ mCancelable = cancelable;
+ updateWindowForCancelable();
+ setOnCancelListener(cancelListener);
+ }
+
+ /**
+ * Retrieve the Context this Dialog is running in.
+ *
+ * @return Context The Context used by the Dialog.
+ */
+ public final @NonNull Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Retrieve the {@link ActionBar} attached to this dialog, if present.
+ *
+ * @return The ActionBar attached to the dialog or null if no ActionBar is present.
+ */
+ public @Nullable ActionBar getActionBar() {
+ return mActionBar;
+ }
+
+ /**
+ * Sets the Activity that owns this dialog. An example use: This Dialog will
+ * use the suggested volume control stream of the Activity.
+ *
+ * @param activity The Activity that owns this dialog.
+ */
+ public final void setOwnerActivity(@NonNull Activity activity) {
+ mOwnerActivity = activity;
+
+ getWindow().setVolumeControlStream(mOwnerActivity.getVolumeControlStream());
+ }
+
+ /**
+ * Returns the Activity that owns this Dialog. For example, if
+ * {@link Activity#showDialog(int)} is used to show this Dialog, that
+ * Activity will be the owner (by default). Depending on how this dialog was
+ * created, this may return null.
+ *
+ * @return The Activity that owns this Dialog.
+ */
+ public final @Nullable Activity getOwnerActivity() {
+ return mOwnerActivity;
+ }
+
+ /**
+ * @return Whether the dialog is currently showing.
+ */
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Forces immediate creation of the dialog.
+ * <p>
+ * Note that you should not override this method to perform dialog creation.
+ * Rather, override {@link #onCreate(Bundle)}.
+ */
+ public void create() {
+ if (!mCreated) {
+ dispatchOnCreate(null);
+ }
+ }
+
+ /**
+ * Start the dialog and display it on screen. The window is placed in the
+ * application layer and opaque. Note that you should not override this
+ * method to do initialization when the dialog is shown, instead implement
+ * that in {@link #onStart}.
+ */
+ public void show() {
+ if (mShowing) {
+ if (mDecor != null) {
+ if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
+ mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
+ }
+ mDecor.setVisibility(View.VISIBLE);
+ }
+ return;
+ }
+
+ mCanceled = false;
+
+ if (!mCreated) {
+ dispatchOnCreate(null);
+ } else {
+ // Fill the DecorView in on any configuration changes that
+ // may have occured while it was removed from the WindowManager.
+ final Configuration config = mContext.getResources().getConfiguration();
+ mWindow.getDecorView().dispatchConfigurationChanged(config);
+ }
+
+ onStart();
+ mDecor = mWindow.getDecorView();
+
+ if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
+ final ApplicationInfo info = mContext.getApplicationInfo();
+ mWindow.setDefaultIcon(info.icon);
+ mWindow.setDefaultLogo(info.logo);
+ mActionBar = new WindowDecorActionBar(this);
+ }
+
+ WindowManager.LayoutParams l = mWindow.getAttributes();
+ if ((l.softInputMode
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
+ WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
+ nl.copyFrom(l);
+ nl.softInputMode |=
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ l = nl;
+ }
+
+ mWindowManager.addView(mDecor, l);
+ mShowing = true;
+
+ sendShowMessage();
+ }
+
+ /**
+ * Hide the dialog, but do not dismiss it.
+ */
+ public void hide() {
+ if (mDecor != null) {
+ mDecor.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Dismiss this dialog, removing it from the screen. This method can be
+ * invoked safely from any thread. Note that you should not override this
+ * method to do cleanup when the dialog is dismissed, instead implement
+ * that in {@link #onStop}.
+ */
+ @Override
+ public void dismiss() {
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ dismissDialog();
+ } else {
+ mHandler.post(mDismissAction);
+ }
+ }
+
+ void dismissDialog() {
+ if (mDecor == null || !mShowing) {
+ return;
+ }
+
+ if (mWindow.isDestroyed()) {
+ Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
+ return;
+ }
+
+ try {
+ mWindowManager.removeViewImmediate(mDecor);
+ } finally {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ mDecor = null;
+ mWindow.closeAllPanels();
+ onStop();
+ mShowing = false;
+
+ sendDismissMessage();
+ }
+ }
+
+ private void sendDismissMessage() {
+ if (mDismissMessage != null) {
+ // Obtain a new message so this dialog can be re-used
+ Message.obtain(mDismissMessage).sendToTarget();
+ }
+ }
+
+ private void sendShowMessage() {
+ if (mShowMessage != null) {
+ // Obtain a new message so this dialog can be re-used
+ Message.obtain(mShowMessage).sendToTarget();
+ }
+ }
+
+ // internal method to make sure mCreated is set properly without requiring
+ // users to call through to super in onCreate
+ void dispatchOnCreate(Bundle savedInstanceState) {
+ if (!mCreated) {
+ onCreate(savedInstanceState);
+ mCreated = true;
+ }
+ }
+
+ /**
+ * Similar to {@link Activity#onCreate}, you should initialize your dialog
+ * in this method, including calling {@link #setContentView}.
+ * @param savedInstanceState If this dialog is being reinitialized after a
+ * the hosting activity was previously shut down, holds the result from
+ * the most recent call to {@link #onSaveInstanceState}, or null if this
+ * is the first time.
+ */
+ protected void onCreate(Bundle savedInstanceState) {
+ }
+
+ /**
+ * Called when the dialog is starting.
+ */
+ protected void onStart() {
+ if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true);
+ }
+
+ /**
+ * Called to tell you that you're stopping.
+ */
+ protected void onStop() {
+ if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false);
+ }
+
+ private static final String DIALOG_SHOWING_TAG = "android:dialogShowing";
+ private static final String DIALOG_HIERARCHY_TAG = "android:dialogHierarchy";
+
+ /**
+ * Saves the state of the dialog into a bundle.
+ *
+ * The default implementation saves the state of its view hierarchy, so you'll
+ * likely want to call through to super if you override this to save additional
+ * state.
+ * @return A bundle with the state of the dialog.
+ */
+ public @NonNull Bundle onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(DIALOG_SHOWING_TAG, mShowing);
+ if (mCreated) {
+ bundle.putBundle(DIALOG_HIERARCHY_TAG, mWindow.saveHierarchyState());
+ }
+ return bundle;
+ }
+
+ /**
+ * Restore the state of the dialog from a previously saved bundle.
+ *
+ * The default implementation restores the state of the dialog's view
+ * hierarchy that was saved in the default implementation of {@link #onSaveInstanceState()},
+ * so be sure to call through to super when overriding unless you want to
+ * do all restoring of state yourself.
+ * @param savedInstanceState The state of the dialog previously saved by
+ * {@link #onSaveInstanceState()}.
+ */
+ public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ final Bundle dialogHierarchyState = savedInstanceState.getBundle(DIALOG_HIERARCHY_TAG);
+ if (dialogHierarchyState == null) {
+ // dialog has never been shown, or onCreated, nothing to restore.
+ return;
+ }
+ dispatchOnCreate(savedInstanceState);
+ mWindow.restoreHierarchyState(dialogHierarchyState);
+ if (savedInstanceState.getBoolean(DIALOG_SHOWING_TAG)) {
+ show();
+ }
+ }
+
+ /**
+ * Retrieve the current Window for the activity. This can be used to
+ * directly access parts of the Window API that are not available
+ * through Activity/Screen.
+ *
+ * @return Window The current window, or null if the activity is not
+ * visual.
+ */
+ public @Nullable Window getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Call {@link android.view.Window#getCurrentFocus} on the
+ * Window if this Activity to return the currently focused view.
+ *
+ * @return View The current View with focus or null.
+ *
+ * @see #getWindow
+ * @see android.view.Window#getCurrentFocus
+ */
+ public @Nullable View getCurrentFocus() {
+ return mWindow != null ? mWindow.getCurrentFocus() : null;
+ }
+
+ /**
+ * Finds the first descendant view with the given ID or {@code null} if the
+ * ID is invalid (< 0), there is no matching view in the hierarchy, or the
+ * dialog has not yet been fully created (for example, via {@link #show()}
+ * or {@link #create()}).
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID if found, or {@code null} otherwise
+ * @see View#findViewById(int)
+ */
+ @Nullable
+ public <T extends View> T findViewById(@IdRes int id) {
+ return mWindow.findViewById(id);
+ }
+
+ /**
+ * Set the screen content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the screen.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ */
+ public void setContentView(@LayoutRes int layoutResID) {
+ mWindow.setContentView(layoutResID);
+ }
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarchy.
+ *
+ * @param view The desired content to display.
+ */
+ public void setContentView(@NonNull View view) {
+ mWindow.setContentView(view);
+ }
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarchy.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
+ mWindow.setContentView(view, params);
+ }
+
+ /**
+ * Add an additional content view to the screen. Added after any existing
+ * ones in the screen -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void addContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
+ mWindow.addContentView(view, params);
+ }
+
+ /**
+ * Set the title text for this dialog's window.
+ *
+ * @param title The new text to display in the title.
+ */
+ public void setTitle(@Nullable CharSequence title) {
+ mWindow.setTitle(title);
+ mWindow.getAttributes().setTitle(title);
+ }
+
+ /**
+ * Set the title text for this dialog's window. The text is retrieved
+ * from the resources with the supplied identifier.
+ *
+ * @param titleId the title's text resource identifier
+ */
+ public void setTitle(@StringRes int titleId) {
+ setTitle(mContext.getText(titleId));
+ }
+
+ /**
+ * A key was pressed down.
+ *
+ * <p>If the focused view didn't want this event, this method is called.
+ *
+ * <p>The default implementation consumed the KEYCODE_BACK to later
+ * handle it in {@link #onKeyUp}.
+ *
+ * @see #onKeyUp
+ * @see android.view.KeyEvent
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ event.startTracking();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
+ * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
+ * the event).
+ */
+ @Override
+ public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * A key was released.
+ *
+ * <p>The default implementation handles KEYCODE_BACK to close the
+ * dialog.
+ *
+ * @see #onKeyDown
+ * @see KeyEvent
+ */
+ @Override
+ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
+ && !event.isCanceled()) {
+ onBackPressed();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ */
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, @NonNull KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when the dialog has detected the user's press of the back
+ * key. The default implementation simply cancels the dialog (only if
+ * it is cancelable), but you can override this to do whatever you want.
+ */
+ public void onBackPressed() {
+ if (mCancelable) {
+ cancel();
+ }
+ }
+
+ /**
+ * Called when a key shortcut event is not handled by any of the views in the Dialog.
+ * Override this method to implement global key shortcuts for the Dialog.
+ * Key shortcuts can also be implemented by setting the
+ * {@link MenuItem#setShortcut(char, char) shortcut} property of menu items.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ * @return True if the key shortcut was handled.
+ */
+ public boolean onKeyShortcut(int keyCode, @NonNull KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a touch screen event was not handled by any of the views
+ * under it. This is most useful to process touch events that happen outside
+ * of your window bounds, where there is no view to receive it.
+ *
+ * @param event The touch screen event being processed.
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation will cancel the dialog when a touch
+ * happens outside of the window bounds.
+ */
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
+ cancel();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when the trackball was moved and not handled by any of the
+ * views inside of the activity. So, for example, if the trackball moves
+ * while focus is on a button, you will receive a call here because
+ * buttons do not normally do anything with trackball events. The call
+ * here happens <em>before</em> trackball movements are converted to
+ * DPAD key events, which then get sent back to the view hierarchy, and
+ * will be processed at the point for things like focus navigation.
+ *
+ * @param event The trackball event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTrackballEvent(@NonNull MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a generic motion event was not handled by any of the
+ * views inside of the dialog.
+ * <p>
+ * Generic motion events describe joystick movements, mouse hovers, track pad
+ * touches, scroll wheel movements and other input events. The
+ * {@link MotionEvent#getSource() source} of the motion event specifies
+ * the class of input that was received. Implementations of this method
+ * must examine the bits in the source before processing the event.
+ * The following code example shows how this is done.
+ * </p><p>
+ * Generic motion events with source class
+ * {@link android.view.InputDevice#SOURCE_CLASS_POINTER}
+ * are delivered to the view under the pointer. All other generic motion events are
+ * delivered to the focused view.
+ * </p><p>
+ * See {@link View#onGenericMotionEvent(MotionEvent)} for an example of how to
+ * handle this event.
+ * </p>
+ *
+ * @param event The generic motion event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
+ if (mDecor != null) {
+ mWindowManager.updateViewLayout(mDecor, params);
+ }
+ }
+
+ @Override
+ public void onContentChanged() {
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ }
+
+ /** @hide */
+ @Override
+ public void onWindowDismissed(boolean finishTask, boolean suppressWindowTransition) {
+ dismiss();
+ }
+
+ /**
+ * Called to process key events. You can override this to intercept all
+ * key events before they are dispatched to the window. Be sure to call
+ * this implementation for key events that should be handled normally.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ @Override
+ public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
+ if ((mOnKeyListener != null) && (mOnKeyListener.onKey(this, event.getKeyCode(), event))) {
+ return true;
+ }
+ if (mWindow.superDispatchKeyEvent(event)) {
+ return true;
+ }
+ return event.dispatch(this, mDecor != null
+ ? mDecor.getKeyDispatcherState() : null, this);
+ }
+
+ /**
+ * Called to process a key shortcut event.
+ * You can override this to intercept all key shortcut events before they are
+ * dispatched to the window. Be sure to call this implementation for key shortcut
+ * events that should be handled normally.
+ *
+ * @param event The key shortcut event.
+ * @return True if this event was consumed.
+ */
+ @Override
+ public boolean dispatchKeyShortcutEvent(@NonNull KeyEvent event) {
+ if (mWindow.superDispatchKeyShortcutEvent(event)) {
+ return true;
+ }
+ return onKeyShortcut(event.getKeyCode(), event);
+ }
+
+ /**
+ * Called to process touch screen events. You can override this to
+ * intercept all touch screen events before they are dispatched to the
+ * window. Be sure to call this implementation for touch screen events
+ * that should be handled normally.
+ *
+ * @param ev The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ @Override
+ public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
+ if (mWindow.superDispatchTouchEvent(ev)) {
+ return true;
+ }
+ return onTouchEvent(ev);
+ }
+
+ /**
+ * Called to process trackball events. You can override this to
+ * intercept all trackball events before they are dispatched to the
+ * window. Be sure to call this implementation for trackball events
+ * that should be handled normally.
+ *
+ * @param ev The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ @Override
+ public boolean dispatchTrackballEvent(@NonNull MotionEvent ev) {
+ if (mWindow.superDispatchTrackballEvent(ev)) {
+ return true;
+ }
+ return onTrackballEvent(ev);
+ }
+
+ /**
+ * Called to process generic motion events. You can override this to
+ * intercept all generic motion events before they are dispatched to the
+ * window. Be sure to call this implementation for generic motion events
+ * that should be handled normally.
+ *
+ * @param ev The generic motion event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ @Override
+ public boolean dispatchGenericMotionEvent(@NonNull MotionEvent ev) {
+ if (mWindow.superDispatchGenericMotionEvent(ev)) {
+ return true;
+ }
+ return onGenericMotionEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) {
+ event.setClassName(getClass().getName());
+ event.setPackageName(mContext.getPackageName());
+
+ LayoutParams params = getWindow().getAttributes();
+ boolean isFullScreen = (params.width == LayoutParams.MATCH_PARENT) &&
+ (params.height == LayoutParams.MATCH_PARENT);
+ event.setFullScreen(isFullScreen);
+
+ return false;
+ }
+
+ /**
+ * @see Activity#onCreatePanelView(int)
+ */
+ @Override
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ /**
+ * @see Activity#onCreatePanelMenu(int, Menu)
+ */
+ @Override
+ public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ return onCreateOptionsMenu(menu);
+ }
+
+ return false;
+ }
+
+ /**
+ * @see Activity#onPreparePanel(int, View, Menu)
+ */
+ @Override
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
+ return onPrepareOptionsMenu(menu) && menu.hasVisibleItems();
+ }
+ return true;
+ }
+
+ /**
+ * @see Activity#onMenuOpened(int, Menu)
+ */
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_ACTION_BAR) {
+ mActionBar.dispatchMenuVisibilityChanged(true);
+ }
+ return true;
+ }
+
+ /**
+ * @see Activity#onMenuItemSelected(int, MenuItem)
+ */
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onPanelClosed(int, Menu)
+ */
+ @Override
+ public void onPanelClosed(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_ACTION_BAR) {
+ mActionBar.dispatchMenuVisibilityChanged(false);
+ }
+ }
+
+ /**
+ * It is usually safe to proxy this call to the owner activity's
+ * {@link Activity#onCreateOptionsMenu(Menu)} if the client desires the same
+ * menu for this Dialog.
+ *
+ * @see Activity#onCreateOptionsMenu(Menu)
+ * @see #getOwnerActivity()
+ */
+ public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+ return true;
+ }
+
+ /**
+ * It is usually safe to proxy this call to the owner activity's
+ * {@link Activity#onPrepareOptionsMenu(Menu)} if the client desires the
+ * same menu for this Dialog.
+ *
+ * @see Activity#onPrepareOptionsMenu(Menu)
+ * @see #getOwnerActivity()
+ */
+ public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
+ return true;
+ }
+
+ /**
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ */
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onOptionsMenuClosed(Menu)
+ */
+ public void onOptionsMenuClosed(@NonNull Menu menu) {
+ }
+
+ /**
+ * @see Activity#openOptionsMenu()
+ */
+ public void openOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL)) {
+ mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
+ }
+ }
+
+ /**
+ * @see Activity#closeOptionsMenu()
+ */
+ public void closeOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL)) {
+ mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL);
+ }
+ }
+
+ /**
+ * @see Activity#invalidateOptionsMenu()
+ */
+ public void invalidateOptionsMenu() {
+ if (mWindow.hasFeature(Window.FEATURE_OPTIONS_PANEL)) {
+ mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL);
+ }
+ }
+
+ /**
+ * @see Activity#onCreateContextMenu(ContextMenu, View, ContextMenuInfo)
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * @see Activity#registerForContextMenu(View)
+ */
+ public void registerForContextMenu(@NonNull View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * @see Activity#unregisterForContextMenu(View)
+ */
+ public void unregisterForContextMenu(@NonNull View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * @see Activity#openContextMenu(View)
+ */
+ public void openContextMenu(@NonNull View view) {
+ view.showContextMenu();
+ }
+
+ /**
+ * @see Activity#onContextItemSelected(MenuItem)
+ */
+ public boolean onContextItemSelected(@NonNull MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onContextMenuClosed(Menu)
+ */
+ public void onContextMenuClosed(@NonNull Menu menu) {
+ }
+
+ /**
+ * This hook is called when the user signals the desire to start a search.
+ */
+ @Override
+ public boolean onSearchRequested(@NonNull SearchEvent searchEvent) {
+ mSearchEvent = searchEvent;
+ return onSearchRequested();
+ }
+
+ /**
+ * This hook is called when the user signals the desire to start a search.
+ */
+ @Override
+ public boolean onSearchRequested() {
+ final SearchManager searchManager = (SearchManager) mContext
+ .getSystemService(Context.SEARCH_SERVICE);
+
+ // associate search with owner activity
+ final ComponentName appName = getAssociatedActivity();
+ if (appName != null && searchManager.getSearchableInfo(appName) != null) {
+ searchManager.startSearch(null, false, appName, null, false);
+ dismiss();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * During the onSearchRequested() callbacks, this function will return the
+ * {@link SearchEvent} that triggered the callback, if it exists.
+ *
+ * @return SearchEvent The SearchEvent that triggered the {@link
+ * #onSearchRequested} callback.
+ */
+ public final @Nullable SearchEvent getSearchEvent() {
+ return mSearchEvent;
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
+ if (mActionBar != null && mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) {
+ return mActionBar.startActionMode(callback);
+ }
+ return null;
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) {
+ try {
+ mActionModeTypeStarting = type;
+ return onWindowStartingActionMode(callback);
+ } finally {
+ mActionModeTypeStarting = ActionMode.TYPE_PRIMARY;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Note that if you override this method you should always call through
+ * to the superclass implementation by calling super.onActionModeStarted(mode).
+ */
+ @Override
+ @CallSuper
+ public void onActionModeStarted(ActionMode mode) {
+ mActionMode = mode;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Note that if you override this method you should always call through
+ * to the superclass implementation by calling super.onActionModeFinished(mode).
+ */
+ @Override
+ @CallSuper
+ public void onActionModeFinished(ActionMode mode) {
+ if (mode == mActionMode) {
+ mActionMode = null;
+ }
+ }
+
+ /**
+ * @return The activity associated with this dialog, or null if there is no associated activity.
+ */
+ private ComponentName getAssociatedActivity() {
+ Activity activity = mOwnerActivity;
+ Context context = getContext();
+ while (activity == null && context != null) {
+ if (context instanceof Activity) {
+ activity = (Activity) context; // found it!
+ } else {
+ context = (context instanceof ContextWrapper) ?
+ ((ContextWrapper) context).getBaseContext() : // unwrap one level
+ null; // done
+ }
+ }
+ return activity == null ? null : activity.getComponentName();
+ }
+
+
+ /**
+ * Request that key events come to this dialog. Use this if your
+ * dialog has no views with focus, but the dialog still wants
+ * a chance to process key events.
+ *
+ * @param get true if the dialog should receive key events, false otherwise
+ * @see android.view.Window#takeKeyEvents
+ */
+ public void takeKeyEvents(boolean get) {
+ mWindow.takeKeyEvents(get);
+ }
+
+ /**
+ * Enable extended window features. This is a convenience for calling
+ * {@link android.view.Window#requestFeature getWindow().requestFeature()}.
+ *
+ * @param featureId The desired feature as defined in
+ * {@link android.view.Window}.
+ * @return Returns true if the requested feature is supported and now
+ * enabled.
+ *
+ * @see android.view.Window#requestFeature
+ */
+ public final boolean requestWindowFeature(int featureId) {
+ return getWindow().requestFeature(featureId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableResource}.
+ */
+ public final void setFeatureDrawableResource(int featureId, @DrawableRes int resId) {
+ getWindow().setFeatureDrawableResource(featureId, resId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableUri}.
+ */
+ public final void setFeatureDrawableUri(int featureId, @Nullable Uri uri) {
+ getWindow().setFeatureDrawableUri(featureId, uri);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawable(int, Drawable)}.
+ */
+ public final void setFeatureDrawable(int featureId, @Nullable Drawable drawable) {
+ getWindow().setFeatureDrawable(featureId, drawable);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableAlpha}.
+ */
+ public final void setFeatureDrawableAlpha(int featureId, int alpha) {
+ getWindow().setFeatureDrawableAlpha(featureId, alpha);
+ }
+
+ public @NonNull LayoutInflater getLayoutInflater() {
+ return getWindow().getLayoutInflater();
+ }
+
+ /**
+ * Sets whether this dialog is cancelable with the
+ * {@link KeyEvent#KEYCODE_BACK BACK} key.
+ */
+ public void setCancelable(boolean flag) {
+ mCancelable = flag;
+ updateWindowForCancelable();
+ }
+
+ /**
+ * Sets whether this dialog is canceled when touched outside the window's
+ * bounds. If setting to true, the dialog is set to be cancelable if not
+ * already set.
+ *
+ * @param cancel Whether the dialog should be canceled when touched outside
+ * the window.
+ */
+ public void setCanceledOnTouchOutside(boolean cancel) {
+ if (cancel && !mCancelable) {
+ mCancelable = true;
+ updateWindowForCancelable();
+ }
+
+ mWindow.setCloseOnTouchOutside(cancel);
+ }
+
+ /**
+ * Cancel the dialog. This is essentially the same as calling {@link #dismiss()}, but it will
+ * also call your {@link DialogInterface.OnCancelListener} (if registered).
+ */
+ @Override
+ public void cancel() {
+ if (!mCanceled && mCancelMessage != null) {
+ mCanceled = true;
+ // Obtain a new message so this dialog can be re-used
+ Message.obtain(mCancelMessage).sendToTarget();
+ }
+ dismiss();
+ }
+
+ /**
+ * Set a listener to be invoked when the dialog is canceled.
+ *
+ * <p>This will only be invoked when the dialog is canceled.
+ * Cancel events alone will not capture all ways that
+ * the dialog might be dismissed. If the creator needs
+ * to know when a dialog is dismissed in general, use
+ * {@link #setOnDismissListener}.</p>
+ *
+ * @param listener The {@link DialogInterface.OnCancelListener} to use.
+ */
+ public void setOnCancelListener(@Nullable OnCancelListener listener) {
+ if (mCancelAndDismissTaken != null) {
+ throw new IllegalStateException(
+ "OnCancelListener is already taken by "
+ + mCancelAndDismissTaken + " and can not be replaced.");
+ }
+ if (listener != null) {
+ mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
+ } else {
+ mCancelMessage = null;
+ }
+ }
+
+ /**
+ * Set a message to be sent when the dialog is canceled.
+ * @param msg The msg to send when the dialog is canceled.
+ * @see #setOnCancelListener(android.content.DialogInterface.OnCancelListener)
+ */
+ public void setCancelMessage(@Nullable Message msg) {
+ mCancelMessage = msg;
+ }
+
+ /**
+ * Set a listener to be invoked when the dialog is dismissed.
+ * @param listener The {@link DialogInterface.OnDismissListener} to use.
+ */
+ public void setOnDismissListener(@Nullable OnDismissListener listener) {
+ if (mCancelAndDismissTaken != null) {
+ throw new IllegalStateException(
+ "OnDismissListener is already taken by "
+ + mCancelAndDismissTaken + " and can not be replaced.");
+ }
+ if (listener != null) {
+ mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
+ } else {
+ mDismissMessage = null;
+ }
+ }
+
+ /**
+ * Sets a listener to be invoked when the dialog is shown.
+ * @param listener The {@link DialogInterface.OnShowListener} to use.
+ */
+ public void setOnShowListener(@Nullable OnShowListener listener) {
+ if (listener != null) {
+ mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
+ } else {
+ mShowMessage = null;
+ }
+ }
+
+ /**
+ * Set a message to be sent when the dialog is dismissed.
+ * @param msg The msg to send when the dialog is dismissed.
+ */
+ public void setDismissMessage(@Nullable Message msg) {
+ mDismissMessage = msg;
+ }
+
+ /** @hide */
+ public boolean takeCancelAndDismissListeners(@Nullable String msg,
+ @Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) {
+ if (mCancelAndDismissTaken != null) {
+ mCancelAndDismissTaken = null;
+ } else if (mCancelMessage != null || mDismissMessage != null) {
+ return false;
+ }
+
+ setOnCancelListener(cancel);
+ setOnDismissListener(dismiss);
+ mCancelAndDismissTaken = msg;
+
+ return true;
+ }
+
+ /**
+ * By default, this will use the owner Activity's suggested stream type.
+ *
+ * @see Activity#setVolumeControlStream(int)
+ * @see #setOwnerActivity(Activity)
+ */
+ public final void setVolumeControlStream(int streamType) {
+ getWindow().setVolumeControlStream(streamType);
+ }
+
+ /**
+ * @see Activity#getVolumeControlStream()
+ */
+ public final int getVolumeControlStream() {
+ return getWindow().getVolumeControlStream();
+ }
+
+ /**
+ * Sets the callback that will be called if a key is dispatched to the dialog.
+ */
+ public void setOnKeyListener(@Nullable OnKeyListener onKeyListener) {
+ mOnKeyListener = onKeyListener;
+ }
+
+ private static final class ListenersHandler extends Handler {
+ private final WeakReference<DialogInterface> mDialog;
+
+ public ListenersHandler(Dialog dialog) {
+ mDialog = new WeakReference<>(dialog);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case DISMISS:
+ ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
+ break;
+ case CANCEL:
+ ((OnCancelListener) msg.obj).onCancel(mDialog.get());
+ break;
+ case SHOW:
+ ((OnShowListener) msg.obj).onShow(mDialog.get());
+ break;
+ }
+ }
+ }
+
+ private void updateWindowForCancelable() {
+ mWindow.setCloseOnSwipeEnabled(mCancelable);
+ }
+}
diff --git a/android/app/DialogFragment.java b/android/app/DialogFragment.java
new file mode 100644
index 00000000..7e0e4d82
--- /dev/null
+++ b/android/app/DialogFragment.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A fragment that displays a dialog window, floating on top of its
+ * activity's window. This fragment contains a Dialog object, which it
+ * displays as appropriate based on the fragment's state. Control of
+ * the dialog (deciding when to show, hide, dismiss it) should be done through
+ * the API here, not with direct calls on the dialog.
+ *
+ * <p>Implementations should override this class and implement
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} to supply the
+ * content of the dialog. Alternatively, they can override
+ * {@link #onCreateDialog(Bundle)} to create an entirely custom dialog, such
+ * as an AlertDialog, with its own content.
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#Lifecycle">Lifecycle</a>
+ * <li><a href="#BasicDialog">Basic Dialog</a>
+ * <li><a href="#AlertDialog">Alert Dialog</a>
+ * <li><a href="#DialogOrEmbed">Selecting Between Dialog or Embedding</a>
+ * </ol>
+ *
+ * <a name="Lifecycle"></a>
+ * <h3>Lifecycle</h3>
+ *
+ * <p>DialogFragment does various things to keep the fragment's lifecycle
+ * driving it, instead of the Dialog. Note that dialogs are generally
+ * autonomous entities -- they are their own window, receiving their own
+ * input events, and often deciding on their own when to disappear (by
+ * receiving a back key event or the user clicking on a button).
+ *
+ * <p>DialogFragment needs to ensure that what is happening with the Fragment
+ * and Dialog states remains consistent. To do this, it watches for dismiss
+ * events from the dialog and takes care of removing its own state when they
+ * happen. This means you should use {@link #show(FragmentManager, String)}
+ * or {@link #show(FragmentTransaction, String)} to add an instance of
+ * DialogFragment to your UI, as these keep track of how DialogFragment should
+ * remove itself when the dialog is dismissed.
+ *
+ * <a name="BasicDialog"></a>
+ * <h3>Basic Dialog</h3>
+ *
+ * <p>The simplest use of DialogFragment is as a floating container for the
+ * fragment's view hierarchy. A simple implementation may look like this:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java
+ * dialog}
+ *
+ * <p>An example showDialog() method on the Activity could be:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialog.java
+ * add_dialog}
+ *
+ * <p>This removes any currently shown dialog, creates a new DialogFragment
+ * with an argument, and shows it as a new state on the back stack. When the
+ * transaction is popped, the current DialogFragment and its Dialog will be
+ * destroyed, and the previous one (if any) re-shown. Note that in this case
+ * DialogFragment will take care of popping the transaction of the Dialog
+ * is dismissed separately from it.
+ *
+ * <a name="AlertDialog"></a>
+ * <h3>Alert Dialog</h3>
+ *
+ * <p>Instead of (or in addition to) implementing {@link #onCreateView} to
+ * generate the view hierarchy inside of a dialog, you may implement
+ * {@link #onCreateDialog(Bundle)} to create your own custom Dialog object.
+ *
+ * <p>This is most useful for creating an {@link AlertDialog}, allowing you
+ * to display standard alerts to the user that are managed by a fragment.
+ * A simple example implementation of this is:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java
+ * dialog}
+ *
+ * <p>The activity creating this fragment may have the following methods to
+ * show the dialog and receive results from it:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentAlertDialog.java
+ * activity}
+ *
+ * <p>Note that in this case the fragment is not placed on the back stack, it
+ * is just added as an indefinitely running fragment. Because dialogs normally
+ * are modal, this will still operate as a back stack, since the dialog will
+ * capture user input until it is dismissed. When it is dismissed, DialogFragment
+ * will take care of removing itself from its fragment manager.
+ *
+ * <a name="DialogOrEmbed"></a>
+ * <h3>Selecting Between Dialog or Embedding</h3>
+ *
+ * <p>A DialogFragment can still optionally be used as a normal fragment, if
+ * desired. This is useful if you have a fragment that in some cases should
+ * be shown as a dialog and others embedded in a larger UI. This behavior
+ * will normally be automatically selected for you based on how you are using
+ * the fragment, but can be customized with {@link #setShowsDialog(boolean)}.
+ *
+ * <p>For example, here is a simple dialog fragment:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
+ * dialog}
+ *
+ * <p>An instance of this fragment can be created and shown as a dialog:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
+ * show_dialog}
+ *
+ * <p>It can also be added as content in a view hierarchy:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
+ * embed}
+ */
+public class DialogFragment extends Fragment
+ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
+
+ /**
+ * Style for {@link #setStyle(int, int)}: a basic,
+ * normal dialog.
+ */
+ public static final int STYLE_NORMAL = 0;
+
+ /**
+ * Style for {@link #setStyle(int, int)}: don't include
+ * a title area.
+ */
+ public static final int STYLE_NO_TITLE = 1;
+
+ /**
+ * Style for {@link #setStyle(int, int)}: don't draw
+ * any frame at all; the view hierarchy returned by {@link #onCreateView}
+ * is entirely responsible for drawing the dialog.
+ */
+ public static final int STYLE_NO_FRAME = 2;
+
+ /**
+ * Style for {@link #setStyle(int, int)}: like
+ * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog.
+ * The user can not touch it, and its window will not receive input focus.
+ */
+ public static final int STYLE_NO_INPUT = 3;
+
+ private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
+ private static final String SAVED_STYLE = "android:style";
+ private static final String SAVED_THEME = "android:theme";
+ private static final String SAVED_CANCELABLE = "android:cancelable";
+ private static final String SAVED_SHOWS_DIALOG = "android:showsDialog";
+ private static final String SAVED_BACK_STACK_ID = "android:backStackId";
+
+ int mStyle = STYLE_NORMAL;
+ int mTheme = 0;
+ boolean mCancelable = true;
+ boolean mShowsDialog = true;
+ int mBackStackId = -1;
+
+ Dialog mDialog;
+ boolean mViewDestroyed;
+ boolean mDismissed;
+ boolean mShownByMe;
+
+ public DialogFragment() {
+ }
+
+ /**
+ * Call to customize the basic appearance and behavior of the
+ * fragment's dialog. This can be used for some common dialog behaviors,
+ * taking care of selecting flags, theme, and other options for you. The
+ * same effect can be achieve by manually setting Dialog and Window
+ * attributes yourself. Calling this after the fragment's Dialog is
+ * created will have no effect.
+ *
+ * @param style Selects a standard style: may be {@link #STYLE_NORMAL},
+ * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or
+ * {@link #STYLE_NO_INPUT}.
+ * @param theme Optional custom theme. If 0, an appropriate theme (based
+ * on the style) will be selected for you.
+ */
+ public void setStyle(int style, int theme) {
+ mStyle = style;
+ if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
+ mTheme = com.android.internal.R.style.Theme_DeviceDefault_Dialog_NoFrame;
+ }
+ if (theme != 0) {
+ mTheme = theme;
+ }
+ }
+
+ /**
+ * Display the dialog, adding the fragment to the given FragmentManager. This
+ * is a convenience for explicitly creating a transaction, adding the
+ * fragment to it with the given tag, and committing it. This does
+ * <em>not</em> add the transaction to the back stack. When the fragment
+ * is dismissed, a new transaction will be executed to remove it from
+ * the activity.
+ * @param manager The FragmentManager this fragment will be added to.
+ * @param tag The tag for this fragment, as per
+ * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
+ */
+ public void show(FragmentManager manager, String tag) {
+ mDismissed = false;
+ mShownByMe = true;
+ FragmentTransaction ft = manager.beginTransaction();
+ ft.add(this, tag);
+ ft.commit();
+ }
+
+ /** {@hide} */
+ public void showAllowingStateLoss(FragmentManager manager, String tag) {
+ mDismissed = false;
+ mShownByMe = true;
+ FragmentTransaction ft = manager.beginTransaction();
+ ft.add(this, tag);
+ ft.commitAllowingStateLoss();
+ }
+
+ /**
+ * Display the dialog, adding the fragment using an existing transaction
+ * and then committing the transaction.
+ * @param transaction An existing transaction in which to add the fragment.
+ * @param tag The tag for this fragment, as per
+ * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
+ * @return Returns the identifier of the committed transaction, as per
+ * {@link FragmentTransaction#commit() FragmentTransaction.commit()}.
+ */
+ public int show(FragmentTransaction transaction, String tag) {
+ mDismissed = false;
+ mShownByMe = true;
+ transaction.add(this, tag);
+ mViewDestroyed = false;
+ mBackStackId = transaction.commit();
+ return mBackStackId;
+ }
+
+ /**
+ * Dismiss the fragment and its dialog. If the fragment was added to the
+ * back stack, all back stack state up to and including this entry will
+ * be popped. Otherwise, a new transaction will be committed to remove
+ * the fragment.
+ */
+ public void dismiss() {
+ dismissInternal(false);
+ }
+
+ /**
+ * Version of {@link #dismiss()} that uses
+ * {@link FragmentTransaction#commitAllowingStateLoss()
+ * FragmentTransaction.commitAllowingStateLoss()}. See linked
+ * documentation for further details.
+ */
+ public void dismissAllowingStateLoss() {
+ dismissInternal(true);
+ }
+
+ void dismissInternal(boolean allowStateLoss) {
+ if (mDismissed) {
+ return;
+ }
+ mDismissed = true;
+ mShownByMe = false;
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+ }
+ mViewDestroyed = true;
+ if (mBackStackId >= 0) {
+ getFragmentManager().popBackStack(mBackStackId,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ mBackStackId = -1;
+ } else {
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.remove(this);
+ if (allowStateLoss) {
+ ft.commitAllowingStateLoss();
+ } else {
+ ft.commit();
+ }
+ }
+ }
+
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+ public int getTheme() {
+ return mTheme;
+ }
+
+ /**
+ * Control whether the shown Dialog is cancelable. Use this instead of
+ * directly calling {@link Dialog#setCancelable(boolean)
+ * Dialog.setCancelable(boolean)}, because DialogFragment needs to change
+ * its behavior based on this.
+ *
+ * @param cancelable If true, the dialog is cancelable. The default
+ * is true.
+ */
+ public void setCancelable(boolean cancelable) {
+ mCancelable = cancelable;
+ if (mDialog != null) mDialog.setCancelable(cancelable);
+ }
+
+ /**
+ * Return the current value of {@link #setCancelable(boolean)}.
+ */
+ public boolean isCancelable() {
+ return mCancelable;
+ }
+
+ /**
+ * Controls whether this fragment should be shown in a dialog. If not
+ * set, no Dialog will be created in {@link #onActivityCreated(Bundle)},
+ * and the fragment's view hierarchy will thus not be added to it. This
+ * allows you to instead use it as a normal fragment (embedded inside of
+ * its activity).
+ *
+ * <p>This is normally set for you based on whether the fragment is
+ * associated with a container view ID passed to
+ * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}.
+ * If the fragment was added with a container, setShowsDialog will be
+ * initialized to false; otherwise, it will be true.
+ *
+ * @param showsDialog If true, the fragment will be displayed in a Dialog.
+ * If false, no Dialog will be created and the fragment's view hierarchly
+ * left undisturbed.
+ */
+ public void setShowsDialog(boolean showsDialog) {
+ mShowsDialog = showsDialog;
+ }
+
+ /**
+ * Return the current value of {@link #setShowsDialog(boolean)}.
+ */
+ public boolean getShowsDialog() {
+ return mShowsDialog;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (!mShownByMe) {
+ // If not explicitly shown through our API, take this as an
+ // indication that the dialog is no longer dismissed.
+ mDismissed = false;
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (!mShownByMe && !mDismissed) {
+ // The fragment was not shown by a direct call here, it is not
+ // dismissed, and now it is being detached... well, okay, thou
+ // art now dismissed. Have fun.
+ mDismissed = true;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mShowsDialog = mContainerId == 0;
+
+ if (savedInstanceState != null) {
+ mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);
+ mTheme = savedInstanceState.getInt(SAVED_THEME, 0);
+ mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);
+ mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
+ mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
+ if (!mShowsDialog) {
+ return super.onGetLayoutInflater(savedInstanceState);
+ }
+
+ mDialog = onCreateDialog(savedInstanceState);
+ switch (mStyle) {
+ case STYLE_NO_INPUT:
+ mDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+ // fall through...
+ case STYLE_NO_FRAME:
+ case STYLE_NO_TITLE:
+ mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ if (mDialog != null) {
+ return (LayoutInflater)mDialog.getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+ return (LayoutInflater) mHost.getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Override to build your own custom Dialog container. This is typically
+ * used to show an AlertDialog instead of a generic Dialog; when doing so,
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need
+ * to be implemented since the AlertDialog takes care of its own content.
+ *
+ * <p>This method will be called after {@link #onCreate(Bundle)} and
+ * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The
+ * default implementation simply instantiates and returns a {@link Dialog}
+ * class.
+ *
+ * <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener
+ * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener
+ * Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em>
+ * To find out about these events, override {@link #onCancel(DialogInterface)}
+ * and {@link #onDismiss(DialogInterface)}.</p>
+ *
+ * @param savedInstanceState The last saved instance state of the Fragment,
+ * or null if this is a freshly created Fragment.
+ *
+ * @return Return a new Dialog instance to be displayed by the Fragment.
+ */
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new Dialog(getActivity(), getTheme());
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ if (!mViewDestroyed) {
+ // Note: we need to use allowStateLoss, because the dialog
+ // dispatches this asynchronously so we can receive the call
+ // after the activity is paused. Worst case, when the user comes
+ // back to the activity they see the dialog again.
+ dismissInternal(true);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (!mShowsDialog) {
+ return;
+ }
+
+ View view = getView();
+ if (view != null) {
+ if (view.getParent() != null) {
+ throw new IllegalStateException(
+ "DialogFragment can not be attached to a container view");
+ }
+ mDialog.setContentView(view);
+ }
+ final Activity activity = getActivity();
+ if (activity != null) {
+ mDialog.setOwnerActivity(activity);
+ }
+ mDialog.setCancelable(mCancelable);
+ if (!mDialog.takeCancelAndDismissListeners("DialogFragment", this, this)) {
+ throw new IllegalStateException(
+ "You can not set Dialog's OnCancelListener or OnDismissListener");
+ }
+ if (savedInstanceState != null) {
+ Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
+ if (dialogState != null) {
+ mDialog.onRestoreInstanceState(dialogState);
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mDialog != null) {
+ mViewDestroyed = false;
+ mDialog.show();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mDialog != null) {
+ Bundle dialogState = mDialog.onSaveInstanceState();
+ if (dialogState != null) {
+ outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
+ }
+ }
+ if (mStyle != STYLE_NORMAL) {
+ outState.putInt(SAVED_STYLE, mStyle);
+ }
+ if (mTheme != 0) {
+ outState.putInt(SAVED_THEME, mTheme);
+ }
+ if (!mCancelable) {
+ outState.putBoolean(SAVED_CANCELABLE, mCancelable);
+ }
+ if (!mShowsDialog) {
+ outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
+ }
+ if (mBackStackId != -1) {
+ outState.putInt(SAVED_BACK_STACK_ID, mBackStackId);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mDialog != null) {
+ mDialog.hide();
+ }
+ }
+
+ /**
+ * Remove dialog.
+ */
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mDialog != null) {
+ // Set removed here because this dismissal is just to hide
+ // the dialog -- we don't want this to cause the fragment to
+ // actually be removed.
+ mViewDestroyed = true;
+ mDialog.dismiss();
+ mDialog = null;
+ }
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.println("DialogFragment:");
+ writer.print(prefix); writer.print(" mStyle="); writer.print(mStyle);
+ writer.print(" mTheme=0x"); writer.println(Integer.toHexString(mTheme));
+ writer.print(prefix); writer.print(" mCancelable="); writer.print(mCancelable);
+ writer.print(" mShowsDialog="); writer.print(mShowsDialog);
+ writer.print(" mBackStackId="); writer.println(mBackStackId);
+ writer.print(prefix); writer.print(" mDialog="); writer.println(mDialog);
+ writer.print(prefix); writer.print(" mViewDestroyed="); writer.print(mViewDestroyed);
+ writer.print(" mDismissed="); writer.print(mDismissed);
+ writer.print(" mShownByMe="); writer.println(mShownByMe);
+ }
+}
diff --git a/android/app/DownloadManager.java b/android/app/DownloadManager.java
new file mode 100644
index 00000000..b444f17c
--- /dev/null
+++ b/android/app/DownloadManager.java
@@ -0,0 +1,1633 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.net.ConnectivityManager;
+import android.net.NetworkPolicyManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.provider.Downloads;
+import android.provider.MediaStore.Images;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The download manager is a system service that handles long-running HTTP downloads. Clients may
+ * request that a URI be downloaded to a particular destination file. The download manager will
+ * conduct the download in the background, taking care of HTTP interactions and retrying downloads
+ * after failures or across connectivity changes and system reboots.
+ * <p>
+ * Apps that request downloads through this API should register a broadcast receiver for
+ * {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running
+ * download in a notification or from the downloads UI.
+ * <p>
+ * Note that the application must have the {@link android.Manifest.permission#INTERNET}
+ * permission to use this class.
+ */
+@SystemService(Context.DOWNLOAD_SERVICE)
+public class DownloadManager {
+
+ /**
+ * An identifier for a particular download, unique across the system. Clients use this ID to
+ * make subsequent calls related to the download.
+ */
+ public final static String COLUMN_ID = Downloads.Impl._ID;
+
+ /**
+ * The client-supplied title for this download. This will be displayed in system notifications.
+ * Defaults to the empty string.
+ */
+ public final static String COLUMN_TITLE = Downloads.Impl.COLUMN_TITLE;
+
+ /**
+ * The client-supplied description of this download. This will be displayed in system
+ * notifications. Defaults to the empty string.
+ */
+ public final static String COLUMN_DESCRIPTION = Downloads.Impl.COLUMN_DESCRIPTION;
+
+ /**
+ * URI to be downloaded.
+ */
+ public final static String COLUMN_URI = Downloads.Impl.COLUMN_URI;
+
+ /**
+ * Internet Media Type of the downloaded file. If no value is provided upon creation, this will
+ * initially be null and will be filled in based on the server's response once the download has
+ * started.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a>
+ */
+ public final static String COLUMN_MEDIA_TYPE = "media_type";
+
+ /**
+ * Total size of the download in bytes. This will initially be -1 and will be filled in once
+ * the download starts.
+ */
+ public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";
+
+ /**
+ * Uri where downloaded file will be stored. If a destination is supplied by client, that URI
+ * will be used here. Otherwise, the value will initially be null and will be filled in with a
+ * generated URI once the download has started.
+ */
+ public final static String COLUMN_LOCAL_URI = "local_uri";
+
+ /**
+ * Path to the downloaded file on disk.
+ * <p>
+ * Note that apps may not have filesystem permissions to directly access
+ * this path. Instead of trying to open this path directly, apps should use
+ * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain access.
+ *
+ * @deprecated apps should transition to using
+ * {@link ContentResolver#openFileDescriptor(Uri, String)}
+ * instead.
+ */
+ @Deprecated
+ public final static String COLUMN_LOCAL_FILENAME = "local_filename";
+
+ /**
+ * Current status of the download, as one of the STATUS_* constants.
+ */
+ public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS;
+
+ /**
+ * Provides more detail on the status of the download. Its meaning depends on the value of
+ * {@link #COLUMN_STATUS}.
+ *
+ * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that
+ * occurred. If an HTTP error occurred, this will hold the HTTP status code as defined in RFC
+ * 2616. Otherwise, it will hold one of the ERROR_* constants.
+ *
+ * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is
+ * paused. It will hold one of the PAUSED_* constants.
+ *
+ * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this
+ * column's value is undefined.
+ *
+ * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616
+ * status codes</a>
+ */
+ public final static String COLUMN_REASON = "reason";
+
+ /**
+ * Number of bytes download so far.
+ */
+ public final static String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far";
+
+ /**
+ * Timestamp when the download was last modified, in {@link System#currentTimeMillis
+ * System.currentTimeMillis()} (wall clock time in UTC).
+ */
+ public final static String COLUMN_LAST_MODIFIED_TIMESTAMP = "last_modified_timestamp";
+
+ /**
+ * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is
+ * used to delete the entries from MediaProvider database when it is deleted from the
+ * downloaded list.
+ */
+ public static final String COLUMN_MEDIAPROVIDER_URI = Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
+
+ /**
+ * @hide
+ */
+ public final static String COLUMN_ALLOW_WRITE = Downloads.Impl.COLUMN_ALLOW_WRITE;
+
+ /**
+ * Value of {@link #COLUMN_STATUS} when the download is waiting to start.
+ */
+ public final static int STATUS_PENDING = 1 << 0;
+
+ /**
+ * Value of {@link #COLUMN_STATUS} when the download is currently running.
+ */
+ public final static int STATUS_RUNNING = 1 << 1;
+
+ /**
+ * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume.
+ */
+ public final static int STATUS_PAUSED = 1 << 2;
+
+ /**
+ * Value of {@link #COLUMN_STATUS} when the download has successfully completed.
+ */
+ public final static int STATUS_SUCCESSFUL = 1 << 3;
+
+ /**
+ * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried).
+ */
+ public final static int STATUS_FAILED = 1 << 4;
+
+ /**
+ * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit
+ * under any other error code.
+ */
+ public final static int ERROR_UNKNOWN = 1000;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any
+ * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and
+ * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate.
+ */
+ public final static int ERROR_FILE_ERROR = 1001;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager
+ * can't handle.
+ */
+ public final static int ERROR_UNHANDLED_HTTP_CODE = 1002;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at
+ * the HTTP level.
+ */
+ public final static int ERROR_HTTP_DATA_ERROR = 1004;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when there were too many redirects.
+ */
+ public final static int ERROR_TOO_MANY_REDIRECTS = 1005;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically,
+ * this is because the SD card is full.
+ */
+ public final static int ERROR_INSUFFICIENT_SPACE = 1006;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically,
+ * this is because the SD card is not mounted.
+ */
+ public final static int ERROR_DEVICE_NOT_FOUND = 1007;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't
+ * resume the download.
+ */
+ public final static int ERROR_CANNOT_RESUME = 1008;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the
+ * download manager will not overwrite an existing file).
+ */
+ public final static int ERROR_FILE_ALREADY_EXISTS = 1009;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the download has failed because of
+ * {@link NetworkPolicyManager} controls on the requesting application.
+ *
+ * @hide
+ */
+ public final static int ERROR_BLOCKED = 1010;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the download is paused because some network error
+ * occurred and the download manager is waiting before retrying the request.
+ */
+ public final static int PAUSED_WAITING_TO_RETRY = 1;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to
+ * proceed.
+ */
+ public final static int PAUSED_WAITING_FOR_NETWORK = 2;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over
+ * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed.
+ */
+ public final static int PAUSED_QUEUED_FOR_WIFI = 3;
+
+ /**
+ * Value of {@link #COLUMN_REASON} when the download is paused for some other reason.
+ */
+ public final static int PAUSED_UNKNOWN = 4;
+
+ /**
+ * Broadcast intent action sent by the download manager when a download completes.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";
+
+ /**
+ * Broadcast intent action sent by the download manager when the user clicks on a running
+ * download, either from a system notification or from the downloads UI.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public final static String ACTION_NOTIFICATION_CLICKED =
+ "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
+
+ /**
+ * Intent action to launch an activity to display all downloads.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public final static String ACTION_VIEW_DOWNLOADS = "android.intent.action.VIEW_DOWNLOADS";
+
+ /**
+ * Intent extra included with {@link #ACTION_VIEW_DOWNLOADS} to start DownloadApp in
+ * sort-by-size mode.
+ */
+ public final static String INTENT_EXTRAS_SORT_BY_SIZE =
+ "android.app.DownloadManager.extra_sortBySize";
+
+ /**
+ * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a
+ * long) of the download that just completed.
+ */
+ public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
+
+ /**
+ * When clicks on multiple notifications are received, the following
+ * provides an array of download ids corresponding to the download notification that was
+ * clicked. It can be retrieved by the receiver of this
+ * Intent using {@link android.content.Intent#getLongArrayExtra(String)}.
+ */
+ public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS = "extra_click_download_ids";
+
+ /** {@hide} */
+ @SystemApi
+ public static final String ACTION_DOWNLOAD_COMPLETED =
+ "android.intent.action.DOWNLOAD_COMPLETED";
+
+ /**
+ * columns to request from DownloadProvider.
+ * @hide
+ */
+ public static final String[] UNDERLYING_COLUMNS = new String[] {
+ Downloads.Impl._ID,
+ Downloads.Impl._DATA + " AS " + COLUMN_LOCAL_FILENAME,
+ Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
+ Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.COLUMN_TITLE,
+ Downloads.Impl.COLUMN_DESCRIPTION,
+ Downloads.Impl.COLUMN_URI,
+ Downloads.Impl.COLUMN_STATUS,
+ Downloads.Impl.COLUMN_FILE_NAME_HINT,
+ Downloads.Impl.COLUMN_MIME_TYPE + " AS " + COLUMN_MEDIA_TYPE,
+ Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + COLUMN_TOTAL_SIZE_BYTES,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION + " AS " + COLUMN_LAST_MODIFIED_TIMESTAMP,
+ Downloads.Impl.COLUMN_CURRENT_BYTES + " AS " + COLUMN_BYTES_DOWNLOADED_SO_FAR,
+ Downloads.Impl.COLUMN_ALLOW_WRITE,
+ /* add the following 'computed' columns to the cursor.
+ * they are not 'returned' by the database, but their inclusion
+ * eliminates need to have lot of methods in CursorTranslator
+ */
+ "'placeholder' AS " + COLUMN_LOCAL_URI,
+ "'placeholder' AS " + COLUMN_REASON
+ };
+
+ /**
+ * This class contains all the information necessary to request a new download. The URI is the
+ * only required parameter.
+ *
+ * Note that the default download destination is a shared volume where the system might delete
+ * your file if it needs to reclaim space for system use. If this is a problem, use a location
+ * on external storage (see {@link #setDestinationUri(Uri)}.
+ */
+ public static class Request {
+ /**
+ * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+ * {@link ConnectivityManager#TYPE_MOBILE}.
+ */
+ public static final int NETWORK_MOBILE = 1 << 0;
+
+ /**
+ * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+ * {@link ConnectivityManager#TYPE_WIFI}.
+ */
+ public static final int NETWORK_WIFI = 1 << 1;
+
+ /**
+ * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+ * {@link ConnectivityManager#TYPE_BLUETOOTH}.
+ * @hide
+ */
+ @Deprecated
+ public static final int NETWORK_BLUETOOTH = 1 << 2;
+
+ private Uri mUri;
+ private Uri mDestinationUri;
+ private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
+ private CharSequence mTitle;
+ private CharSequence mDescription;
+ private String mMimeType;
+ private int mAllowedNetworkTypes = ~0; // default to all network types allowed
+ private boolean mRoamingAllowed = true;
+ private boolean mMeteredAllowed = true;
+ private int mFlags = 0;
+ private boolean mIsVisibleInDownloadsUi = true;
+ private boolean mScannable = false;
+ /** if a file is designated as a MediaScanner scannable file, the following value is
+ * stored in the database column {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
+ */
+ private static final int SCANNABLE_VALUE_YES = 0;
+ // value of 1 is stored in the above column by DownloadProvider after it is scanned by
+ // MediaScanner
+ /** if a file is designated as a file that should not be scanned by MediaScanner,
+ * the following value is stored in the database column
+ * {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
+ */
+ private static final int SCANNABLE_VALUE_NO = 2;
+
+ /**
+ * This download is visible but only shows in the notifications
+ * while it's in progress.
+ */
+ public static final int VISIBILITY_VISIBLE = 0;
+
+ /**
+ * This download is visible and shows in the notifications while
+ * in progress and after completion.
+ */
+ public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
+
+ /**
+ * This download doesn't show in the UI or in the notifications.
+ */
+ public static final int VISIBILITY_HIDDEN = 2;
+
+ /**
+ * This download shows in the notifications after completion ONLY.
+ * It is usuable only with
+ * {@link DownloadManager#addCompletedDownload(String, String,
+ * boolean, String, String, long, boolean)}.
+ */
+ public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;
+
+ /** can take any of the following values: {@link #VISIBILITY_HIDDEN}
+ * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}, {@link #VISIBILITY_VISIBLE},
+ * {@link #VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION}
+ */
+ private int mNotificationVisibility = VISIBILITY_VISIBLE;
+
+ /**
+ * @param uri the HTTP or HTTPS URI to download.
+ */
+ public Request(Uri uri) {
+ if (uri == null) {
+ throw new NullPointerException();
+ }
+ String scheme = uri.getScheme();
+ if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
+ throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri);
+ }
+ mUri = uri;
+ }
+
+ Request(String uriString) {
+ mUri = Uri.parse(uriString);
+ }
+
+ /**
+ * Set the local destination for the downloaded file. Must be a file URI to a path on
+ * external storage, and the calling application must have the WRITE_EXTERNAL_STORAGE
+ * permission.
+ * <p>
+ * The downloaded file is not scanned by MediaScanner.
+ * But it can be made scannable by calling {@link #allowScanningByMediaScanner()}.
+ * <p>
+ * By default, downloads are saved to a generated filename in the shared download cache and
+ * may be deleted by the system at any time to reclaim space.
+ *
+ * @return this object
+ */
+ public Request setDestinationUri(Uri uri) {
+ mDestinationUri = uri;
+ return this;
+ }
+
+ /**
+ * Set the local destination for the downloaded file to a path within
+ * the application's external files directory (as returned by
+ * {@link Context#getExternalFilesDir(String)}.
+ * <p>
+ * The downloaded file is not scanned by MediaScanner. But it can be
+ * made scannable by calling {@link #allowScanningByMediaScanner()}.
+ *
+ * @param context the {@link Context} to use in determining the external
+ * files directory
+ * @param dirType the directory type to pass to
+ * {@link Context#getExternalFilesDir(String)}
+ * @param subPath the path within the external directory, including the
+ * destination filename
+ * @return this object
+ * @throws IllegalStateException If the external storage directory
+ * cannot be found or created.
+ */
+ public Request setDestinationInExternalFilesDir(Context context, String dirType,
+ String subPath) {
+ final File file = context.getExternalFilesDir(dirType);
+ if (file == null) {
+ throw new IllegalStateException("Failed to get external storage files directory");
+ } else if (file.exists()) {
+ if (!file.isDirectory()) {
+ throw new IllegalStateException(file.getAbsolutePath() +
+ " already exists and is not a directory");
+ }
+ } else {
+ if (!file.mkdirs()) {
+ throw new IllegalStateException("Unable to create directory: "+
+ file.getAbsolutePath());
+ }
+ }
+ setDestinationFromBase(file, subPath);
+ return this;
+ }
+
+ /**
+ * Set the local destination for the downloaded file to a path within
+ * the public external storage directory (as returned by
+ * {@link Environment#getExternalStoragePublicDirectory(String)}).
+ * <p>
+ * The downloaded file is not scanned by MediaScanner. But it can be
+ * made scannable by calling {@link #allowScanningByMediaScanner()}.
+ *
+ * @param dirType the directory type to pass to {@link Environment#getExternalStoragePublicDirectory(String)}
+ * @param subPath the path within the external directory, including the
+ * destination filename
+ * @return this object
+ * @throws IllegalStateException If the external storage directory
+ * cannot be found or created.
+ */
+ public Request setDestinationInExternalPublicDir(String dirType, String subPath) {
+ File file = Environment.getExternalStoragePublicDirectory(dirType);
+ if (file == null) {
+ throw new IllegalStateException("Failed to get external storage public directory");
+ } else if (file.exists()) {
+ if (!file.isDirectory()) {
+ throw new IllegalStateException(file.getAbsolutePath() +
+ " already exists and is not a directory");
+ }
+ } else {
+ if (!file.mkdirs()) {
+ throw new IllegalStateException("Unable to create directory: "+
+ file.getAbsolutePath());
+ }
+ }
+ setDestinationFromBase(file, subPath);
+ return this;
+ }
+
+ private void setDestinationFromBase(File base, String subPath) {
+ if (subPath == null) {
+ throw new NullPointerException("subPath cannot be null");
+ }
+ mDestinationUri = Uri.withAppendedPath(Uri.fromFile(base), subPath);
+ }
+
+ /**
+ * If the file to be downloaded is to be scanned by MediaScanner, this method
+ * should be called before {@link DownloadManager#enqueue(Request)} is called.
+ */
+ public void allowScanningByMediaScanner() {
+ mScannable = true;
+ }
+
+ /**
+ * Add an HTTP header to be included with the download request. The header will be added to
+ * the end of the list.
+ * @param header HTTP header name
+ * @param value header value
+ * @return this object
+ * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">HTTP/1.1
+ * Message Headers</a>
+ */
+ public Request addRequestHeader(String header, String value) {
+ if (header == null) {
+ throw new NullPointerException("header cannot be null");
+ }
+ if (header.contains(":")) {
+ throw new IllegalArgumentException("header may not contain ':'");
+ }
+ if (value == null) {
+ value = "";
+ }
+ mRequestHeaders.add(Pair.create(header, value));
+ return this;
+ }
+
+ /**
+ * Set the title of this download, to be displayed in notifications (if enabled). If no
+ * title is given, a default one will be assigned based on the download filename, once the
+ * download starts.
+ * @return this object
+ */
+ public Request setTitle(CharSequence title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Set a description of this download, to be displayed in notifications (if enabled)
+ * @return this object
+ */
+ public Request setDescription(CharSequence description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Set the MIME content type of this download. This will override the content type declared
+ * in the server's response.
+ * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1
+ * Media Types</a>
+ * @return this object
+ */
+ public Request setMimeType(String mimeType) {
+ mMimeType = mimeType;
+ return this;
+ }
+
+ /**
+ * Control whether a system notification is posted by the download manager while this
+ * download is running. If enabled, the download manager posts notifications about downloads
+ * through the system {@link android.app.NotificationManager}. By default, a notification is
+ * shown.
+ *
+ * If set to false, this requires the permission
+ * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
+ *
+ * @param show whether the download manager should show a notification for this download.
+ * @return this object
+ * @deprecated use {@link #setNotificationVisibility(int)}
+ */
+ @Deprecated
+ public Request setShowRunningNotification(boolean show) {
+ return (show) ? setNotificationVisibility(VISIBILITY_VISIBLE) :
+ setNotificationVisibility(VISIBILITY_HIDDEN);
+ }
+
+ /**
+ * Control whether a system notification is posted by the download manager while this
+ * download is running or when it is completed.
+ * If enabled, the download manager posts notifications about downloads
+ * through the system {@link android.app.NotificationManager}.
+ * By default, a notification is shown only when the download is in progress.
+ *<p>
+ * It can take the following values: {@link #VISIBILITY_HIDDEN},
+ * {@link #VISIBILITY_VISIBLE},
+ * {@link #VISIBILITY_VISIBLE_NOTIFY_COMPLETED}.
+ *<p>
+ * If set to {@link #VISIBILITY_HIDDEN}, this requires the permission
+ * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
+ *
+ * @param visibility the visibility setting value
+ * @return this object
+ */
+ public Request setNotificationVisibility(int visibility) {
+ mNotificationVisibility = visibility;
+ return this;
+ }
+
+ /**
+ * Restrict the types of networks over which this download may proceed.
+ * By default, all network types are allowed. Consider using
+ * {@link #setAllowedOverMetered(boolean)} instead, since it's more
+ * flexible.
+ * <p>
+ * As of {@link android.os.Build.VERSION_CODES#N}, setting only the
+ * {@link #NETWORK_WIFI} flag here is equivalent to calling
+ * {@link #setAllowedOverMetered(boolean)} with {@code false}.
+ *
+ * @param flags any combination of the NETWORK_* bit flags.
+ * @return this object
+ */
+ public Request setAllowedNetworkTypes(int flags) {
+ mAllowedNetworkTypes = flags;
+ return this;
+ }
+
+ /**
+ * Set whether this download may proceed over a roaming connection. By default, roaming is
+ * allowed.
+ * @param allowed whether to allow a roaming connection to be used
+ * @return this object
+ */
+ public Request setAllowedOverRoaming(boolean allowed) {
+ mRoamingAllowed = allowed;
+ return this;
+ }
+
+ /**
+ * Set whether this download may proceed over a metered network
+ * connection. By default, metered networks are allowed.
+ *
+ * @see ConnectivityManager#isActiveNetworkMetered()
+ */
+ public Request setAllowedOverMetered(boolean allow) {
+ mMeteredAllowed = allow;
+ return this;
+ }
+
+ /**
+ * Specify that to run this download, the device needs to be plugged in.
+ * This defaults to false.
+ *
+ * @param requiresCharging Whether or not the device is plugged in.
+ * @see android.app.job.JobInfo.Builder#setRequiresCharging(boolean)
+ */
+ public Request setRequiresCharging(boolean requiresCharging) {
+ if (requiresCharging) {
+ mFlags |= Downloads.Impl.FLAG_REQUIRES_CHARGING;
+ } else {
+ mFlags &= ~Downloads.Impl.FLAG_REQUIRES_CHARGING;
+ }
+ return this;
+ }
+
+ /**
+ * Specify that to run, the download needs the device to be in idle
+ * mode. This defaults to false.
+ * <p>
+ * Idle mode is a loose definition provided by the system, which means
+ * that the device is not in use, and has not been in use for some time.
+ *
+ * @param requiresDeviceIdle Whether or not the device need be within an
+ * idle maintenance window.
+ * @see android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)
+ */
+ public Request setRequiresDeviceIdle(boolean requiresDeviceIdle) {
+ if (requiresDeviceIdle) {
+ mFlags |= Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
+ } else {
+ mFlags &= ~Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
+ }
+ return this;
+ }
+
+ /**
+ * Set whether this download should be displayed in the system's Downloads UI. True by
+ * default.
+ * @param isVisible whether to display this download in the Downloads UI
+ * @return this object
+ */
+ public Request setVisibleInDownloadsUi(boolean isVisible) {
+ mIsVisibleInDownloadsUi = isVisible;
+ return this;
+ }
+
+ /**
+ * @return ContentValues to be passed to DownloadProvider.insert()
+ */
+ ContentValues toContentValues(String packageName) {
+ ContentValues values = new ContentValues();
+ assert mUri != null;
+ values.put(Downloads.Impl.COLUMN_URI, mUri.toString());
+ values.put(Downloads.Impl.COLUMN_IS_PUBLIC_API, true);
+ values.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, packageName);
+
+ if (mDestinationUri != null) {
+ values.put(Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.DESTINATION_FILE_URI);
+ values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT,
+ mDestinationUri.toString());
+ } else {
+ values.put(Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
+ }
+ // is the file supposed to be media-scannable?
+ values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, (mScannable) ? SCANNABLE_VALUE_YES :
+ SCANNABLE_VALUE_NO);
+
+ if (!mRequestHeaders.isEmpty()) {
+ encodeHttpHeaders(values);
+ }
+
+ putIfNonNull(values, Downloads.Impl.COLUMN_TITLE, mTitle);
+ putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription);
+ putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
+
+ values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility);
+ values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes);
+ values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed);
+ values.put(Downloads.Impl.COLUMN_ALLOW_METERED, mMeteredAllowed);
+ values.put(Downloads.Impl.COLUMN_FLAGS, mFlags);
+ values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, mIsVisibleInDownloadsUi);
+
+ return values;
+ }
+
+ private void encodeHttpHeaders(ContentValues values) {
+ int index = 0;
+ for (Pair<String, String> header : mRequestHeaders) {
+ String headerString = header.first + ": " + header.second;
+ values.put(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX + index, headerString);
+ index++;
+ }
+ }
+
+ private void putIfNonNull(ContentValues contentValues, String key, Object value) {
+ if (value != null) {
+ contentValues.put(key, value.toString());
+ }
+ }
+ }
+
+ /**
+ * This class may be used to filter download manager queries.
+ */
+ public static class Query {
+ /**
+ * Constant for use with {@link #orderBy}
+ * @hide
+ */
+ public static final int ORDER_ASCENDING = 1;
+
+ /**
+ * Constant for use with {@link #orderBy}
+ * @hide
+ */
+ public static final int ORDER_DESCENDING = 2;
+
+ private long[] mIds = null;
+ private Integer mStatusFlags = null;
+ private String mFilterString = null;
+ private String mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
+ private int mOrderDirection = ORDER_DESCENDING;
+ private boolean mOnlyIncludeVisibleInDownloadsUi = false;
+
+ /**
+ * Include only the downloads with the given IDs.
+ * @return this object
+ */
+ public Query setFilterById(long... ids) {
+ mIds = ids;
+ return this;
+ }
+
+ /**
+ *
+ * Include only the downloads that contains the given string in its name.
+ * @return this object
+ * @hide
+ */
+ public Query setFilterByString(@Nullable String filter) {
+ mFilterString = filter;
+ return this;
+ }
+
+ /**
+ * Include only downloads with status matching any the given status flags.
+ * @param flags any combination of the STATUS_* bit flags
+ * @return this object
+ */
+ public Query setFilterByStatus(int flags) {
+ mStatusFlags = flags;
+ return this;
+ }
+
+ /**
+ * Controls whether this query includes downloads not visible in the system's Downloads UI.
+ * @param value if true, this query will only include downloads that should be displayed in
+ * the system's Downloads UI; if false (the default), this query will include
+ * both visible and invisible downloads.
+ * @return this object
+ * @hide
+ */
+ public Query setOnlyIncludeVisibleInDownloadsUi(boolean value) {
+ mOnlyIncludeVisibleInDownloadsUi = value;
+ return this;
+ }
+
+ /**
+ * Change the sort order of the returned Cursor.
+ *
+ * @param column one of the COLUMN_* constants; currently, only
+ * {@link #COLUMN_LAST_MODIFIED_TIMESTAMP} and {@link #COLUMN_TOTAL_SIZE_BYTES} are
+ * supported.
+ * @param direction either {@link #ORDER_ASCENDING} or {@link #ORDER_DESCENDING}
+ * @return this object
+ * @hide
+ */
+ public Query orderBy(String column, int direction) {
+ if (direction != ORDER_ASCENDING && direction != ORDER_DESCENDING) {
+ throw new IllegalArgumentException("Invalid direction: " + direction);
+ }
+
+ if (column.equals(COLUMN_LAST_MODIFIED_TIMESTAMP)) {
+ mOrderByColumn = Downloads.Impl.COLUMN_LAST_MODIFICATION;
+ } else if (column.equals(COLUMN_TOTAL_SIZE_BYTES)) {
+ mOrderByColumn = Downloads.Impl.COLUMN_TOTAL_BYTES;
+ } else {
+ throw new IllegalArgumentException("Cannot order by " + column);
+ }
+ mOrderDirection = direction;
+ return this;
+ }
+
+ /**
+ * Run this query using the given ContentResolver.
+ * @param projection the projection to pass to ContentResolver.query()
+ * @return the Cursor returned by ContentResolver.query()
+ */
+ Cursor runQuery(ContentResolver resolver, String[] projection, Uri baseUri) {
+ Uri uri = baseUri;
+ List<String> selectionParts = new ArrayList<String>();
+ String[] selectionArgs = null;
+
+ int whereArgsCount = (mIds == null) ? 0 : mIds.length;
+ whereArgsCount = (mFilterString == null) ? whereArgsCount : whereArgsCount + 1;
+ selectionArgs = new String[whereArgsCount];
+
+ if (whereArgsCount > 0) {
+ if (mIds != null) {
+ selectionParts.add(getWhereClauseForIds(mIds));
+ getWhereArgsForIds(mIds, selectionArgs);
+ }
+
+ if (mFilterString != null) {
+ selectionParts.add(Downloads.Impl.COLUMN_TITLE + " LIKE ?");
+ selectionArgs[selectionArgs.length - 1] = "%" + mFilterString + "%";
+ }
+ }
+
+ if (mStatusFlags != null) {
+ List<String> parts = new ArrayList<String>();
+ if ((mStatusFlags & STATUS_PENDING) != 0) {
+ parts.add(statusClause("=", Downloads.Impl.STATUS_PENDING));
+ }
+ if ((mStatusFlags & STATUS_RUNNING) != 0) {
+ parts.add(statusClause("=", Downloads.Impl.STATUS_RUNNING));
+ }
+ if ((mStatusFlags & STATUS_PAUSED) != 0) {
+ parts.add(statusClause("=", Downloads.Impl.STATUS_PAUSED_BY_APP));
+ parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_TO_RETRY));
+ parts.add(statusClause("=", Downloads.Impl.STATUS_WAITING_FOR_NETWORK));
+ parts.add(statusClause("=", Downloads.Impl.STATUS_QUEUED_FOR_WIFI));
+ }
+ if ((mStatusFlags & STATUS_SUCCESSFUL) != 0) {
+ parts.add(statusClause("=", Downloads.Impl.STATUS_SUCCESS));
+ }
+ if ((mStatusFlags & STATUS_FAILED) != 0) {
+ parts.add("(" + statusClause(">=", 400)
+ + " AND " + statusClause("<", 600) + ")");
+ }
+ selectionParts.add(joinStrings(" OR ", parts));
+ }
+
+ if (mOnlyIncludeVisibleInDownloadsUi) {
+ selectionParts.add(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + " != '0'");
+ }
+
+ // only return rows which are not marked 'deleted = 1'
+ selectionParts.add(Downloads.Impl.COLUMN_DELETED + " != '1'");
+
+ String selection = joinStrings(" AND ", selectionParts);
+ String orderDirection = (mOrderDirection == ORDER_ASCENDING ? "ASC" : "DESC");
+ String orderBy = mOrderByColumn + " " + orderDirection;
+
+ return resolver.query(uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ private String joinStrings(String joiner, Iterable<String> parts) {
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ for (String part : parts) {
+ if (!first) {
+ builder.append(joiner);
+ }
+ builder.append(part);
+ first = false;
+ }
+ return builder.toString();
+ }
+
+ private String statusClause(String operator, int value) {
+ return Downloads.Impl.COLUMN_STATUS + operator + "'" + value + "'";
+ }
+ }
+
+ private final ContentResolver mResolver;
+ private final String mPackageName;
+
+ private Uri mBaseUri = Downloads.Impl.CONTENT_URI;
+ private boolean mAccessFilename;
+
+ /**
+ * @hide
+ */
+ public DownloadManager(Context context) {
+ mResolver = context.getContentResolver();
+ mPackageName = context.getPackageName();
+
+ // Callers can access filename columns when targeting old platform
+ // versions; otherwise we throw telling them it's deprecated.
+ mAccessFilename = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N;
+ }
+
+ /**
+ * Makes this object access the download provider through /all_downloads URIs rather than
+ * /my_downloads URIs, for clients that have permission to do so.
+ * @hide
+ */
+ public void setAccessAllDownloads(boolean accessAllDownloads) {
+ if (accessAllDownloads) {
+ mBaseUri = Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
+ } else {
+ mBaseUri = Downloads.Impl.CONTENT_URI;
+ }
+ }
+
+ /** {@hide} */
+ public void setAccessFilename(boolean accessFilename) {
+ mAccessFilename = accessFilename;
+ }
+
+ /**
+ * Enqueue a new download. The download will start automatically once the download manager is
+ * ready to execute it and connectivity is available.
+ *
+ * @param request the parameters specifying this download
+ * @return an ID for the download, unique across the system. This ID is used to make future
+ * calls related to this download.
+ */
+ public long enqueue(Request request) {
+ ContentValues values = request.toContentValues(mPackageName);
+ Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
+ long id = Long.parseLong(downloadUri.getLastPathSegment());
+ return id;
+ }
+
+ /**
+ * Marks the specified download as 'to be deleted'. This is done when a completed download
+ * is to be removed but the row was stored without enough info to delete the corresponding
+ * metadata from Mediaprovider database. Actual cleanup of this row is done in DownloadService.
+ *
+ * @param ids the IDs of the downloads to be marked 'deleted'
+ * @return the number of downloads actually updated
+ * @hide
+ */
+ public int markRowDeleted(long... ids) {
+ if (ids == null || ids.length == 0) {
+ // called with nothing to remove!
+ throw new IllegalArgumentException("input param 'ids' can't be null");
+ }
+ return mResolver.delete(mBaseUri, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
+ }
+
+ /**
+ * Cancel downloads and remove them from the download manager. Each download will be stopped if
+ * it was running, and it will no longer be accessible through the download manager.
+ * If there is a downloaded file, partial or complete, it is deleted.
+ *
+ * @param ids the IDs of the downloads to remove
+ * @return the number of downloads actually removed
+ */
+ public int remove(long... ids) {
+ return markRowDeleted(ids);
+ }
+
+ /**
+ * Query the download manager about downloads that have been requested.
+ * @param query parameters specifying filters for this query
+ * @return a Cursor over the result set of downloads, with columns consisting of all the
+ * COLUMN_* constants.
+ */
+ public Cursor query(Query query) {
+ Cursor underlyingCursor = query.runQuery(mResolver, UNDERLYING_COLUMNS, mBaseUri);
+ if (underlyingCursor == null) {
+ return null;
+ }
+ return new CursorTranslator(underlyingCursor, mBaseUri, mAccessFilename);
+ }
+
+ /**
+ * Open a downloaded file for reading. The download must have completed.
+ * @param id the ID of the download
+ * @return a read-only {@link ParcelFileDescriptor}
+ * @throws FileNotFoundException if the destination file does not already exist
+ */
+ public ParcelFileDescriptor openDownloadedFile(long id) throws FileNotFoundException {
+ return mResolver.openFileDescriptor(getDownloadUri(id), "r");
+ }
+
+ /**
+ * Returns the {@link Uri} of the given downloaded file id, if the file is
+ * downloaded successfully. Otherwise, null is returned.
+ *
+ * @param id the id of the downloaded file.
+ * @return the {@link Uri} of the given downloaded file id, if download was
+ * successful. null otherwise.
+ */
+ public Uri getUriForDownloadedFile(long id) {
+ // to check if the file is in cache, get its destination from the database
+ Query query = new Query().setFilterById(id);
+ Cursor cursor = null;
+ try {
+ cursor = query(query);
+ if (cursor == null) {
+ return null;
+ }
+ if (cursor.moveToFirst()) {
+ int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
+ if (DownloadManager.STATUS_SUCCESSFUL == status) {
+ return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ // downloaded file not found or its status is not 'successfully completed'
+ return null;
+ }
+
+ /**
+ * Returns the media type of the given downloaded file id, if the file was
+ * downloaded successfully. Otherwise, null is returned.
+ *
+ * @param id the id of the downloaded file.
+ * @return the media type of the given downloaded file id, if download was successful. null
+ * otherwise.
+ */
+ public String getMimeTypeForDownloadedFile(long id) {
+ Query query = new Query().setFilterById(id);
+ Cursor cursor = null;
+ try {
+ cursor = query(query);
+ if (cursor == null) {
+ return null;
+ }
+ while (cursor.moveToFirst()) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ // downloaded file not found or its status is not 'successfully completed'
+ return null;
+ }
+
+ /**
+ * Restart the given downloads, which must have already completed (successfully or not). This
+ * method will only work when called from within the download manager's process.
+ * @param ids the IDs of the downloads
+ * @hide
+ */
+ public void restartDownload(long... ids) {
+ Cursor cursor = query(new Query().setFilterById(ids));
+ try {
+ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+ int status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS));
+ if (status != STATUS_SUCCESSFUL && status != STATUS_FAILED) {
+ throw new IllegalArgumentException("Cannot restart incomplete download: "
+ + cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
+ values.putNull(Downloads.Impl._DATA);
+ values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
+ values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, 0);
+ mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
+ }
+
+ /**
+ * Force the given downloads to proceed even if their size is larger than
+ * {@link #getMaxBytesOverMobile(Context)}.
+ *
+ * @hide
+ */
+ public void forceDownload(long... ids) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
+ values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
+ values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, 1);
+ mResolver.update(mBaseUri, values, getWhereClauseForIds(ids), getWhereArgsForIds(ids));
+ }
+
+ /**
+ * Returns maximum size, in bytes, of downloads that may go over a mobile connection; or null if
+ * there's no limit
+ *
+ * @param context the {@link Context} to use for accessing the {@link ContentResolver}
+ * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if
+ * there's no limit
+ */
+ public static Long getMaxBytesOverMobile(Context context) {
+ try {
+ return Settings.Global.getLong(context.getContentResolver(),
+ Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
+ } catch (SettingNotFoundException exc) {
+ return null;
+ }
+ }
+
+ /**
+ * Rename the given download if the download has completed
+ *
+ * @param context the {@link Context} to use in case need to update MediaProvider
+ * @param id the downloaded id
+ * @param displayName the new name to rename to
+ * @return true if rename was successful, false otherwise
+ * @hide
+ */
+ public boolean rename(Context context, long id, String displayName) {
+ if (!FileUtils.isValidFatFilename(displayName)) {
+ throw new SecurityException(displayName + " is not a valid filename");
+ }
+
+ Query query = new Query().setFilterById(id);
+ Cursor cursor = null;
+ String oldDisplayName = null;
+ String mimeType = null;
+ try {
+ cursor = query(query);
+ if (cursor == null) {
+ return false;
+ }
+ if (cursor.moveToFirst()) {
+ int status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS));
+ if (DownloadManager.STATUS_SUCCESSFUL != status) {
+ return false;
+ }
+ oldDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TITLE));
+ mimeType = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MEDIA_TYPE));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ if (oldDisplayName == null || mimeType == null) {
+ throw new IllegalStateException(
+ "Document with id " + id + " does not exist");
+ }
+
+ final File parent = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS);
+
+ final File before = new File(parent, oldDisplayName);
+ final File after = new File(parent, displayName);
+
+ if (after.exists()) {
+ throw new IllegalStateException("Already exists " + after);
+ }
+ if (!before.renameTo(after)) {
+ throw new IllegalStateException("Failed to rename to " + after);
+ }
+
+ // Update MediaProvider if necessary
+ if (mimeType.startsWith("image/")) {
+ context.getContentResolver().delete(Images.Media.EXTERNAL_CONTENT_URI,
+ Images.Media.DATA + "=?",
+ new String[] {
+ before.getAbsolutePath()
+ });
+
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(after));
+ context.sendBroadcast(intent);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_TITLE, displayName);
+ values.put(Downloads.Impl._DATA, after.toString());
+ values.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ long[] ids = {id};
+
+ return (mResolver.update(mBaseUri, values, getWhereClauseForIds(ids),
+ getWhereArgsForIds(ids)) == 1);
+ }
+
+ /**
+ * Returns recommended maximum size, in bytes, of downloads that may go over a mobile
+ * connection; or null if there's no recommended limit. The user will have the option to bypass
+ * this limit.
+ *
+ * @param context the {@link Context} to use for accessing the {@link ContentResolver}
+ * @return recommended maximum size, in bytes, of downloads that may go over a mobile
+ * connection; or null if there's no recommended limit.
+ */
+ public static Long getRecommendedMaxBytesOverMobile(Context context) {
+ try {
+ return Settings.Global.getLong(context.getContentResolver(),
+ Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
+ } catch (SettingNotFoundException exc) {
+ return null;
+ }
+ }
+
+ /** {@hide} */
+ public static boolean isActiveNetworkExpensive(Context context) {
+ // TODO: connect to NetworkPolicyManager
+ return false;
+ }
+
+ /** {@hide} */
+ public static long getActiveNetworkWarningBytes(Context context) {
+ // TODO: connect to NetworkPolicyManager
+ return -1;
+ }
+
+ /**
+ * Adds a file to the downloads database system, so it could appear in Downloads App
+ * (and thus become eligible for management by the Downloads App).
+ * <p>
+ * It is helpful to make the file scannable by MediaScanner by setting the param
+ * isMediaScannerScannable to true. It makes the file visible in media managing
+ * applications such as Gallery App, which could be a useful purpose of using this API.
+ *
+ * @param title the title that would appear for this file in Downloads App.
+ * @param description the description that would appear for this file in Downloads App.
+ * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files
+ * scanned by MediaScanner appear in the applications used to view media (for example,
+ * Gallery app).
+ * @param mimeType mimetype of the file.
+ * @param path absolute pathname to the file. The file should be world-readable, so that it can
+ * be managed by the Downloads App and any other app that is used to read it (for example,
+ * Gallery app to display the file, if the file contents represent a video/image).
+ * @param length length of the downloaded file
+ * @param showNotification true if a notification is to be sent, false otherwise
+ * @return an ID for the download entry added to the downloads app, unique across the system
+ * This ID is used to make future calls related to this download.
+ */
+ public long addCompletedDownload(String title, String description,
+ boolean isMediaScannerScannable, String mimeType, String path, long length,
+ boolean showNotification) {
+ return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
+ length, showNotification, false, null, null);
+ }
+
+ /**
+ * Adds a file to the downloads database system, so it could appear in Downloads App
+ * (and thus become eligible for management by the Downloads App).
+ * <p>
+ * It is helpful to make the file scannable by MediaScanner by setting the param
+ * isMediaScannerScannable to true. It makes the file visible in media managing
+ * applications such as Gallery App, which could be a useful purpose of using this API.
+ *
+ * @param title the title that would appear for this file in Downloads App.
+ * @param description the description that would appear for this file in Downloads App.
+ * @param isMediaScannerScannable true if the file is to be scanned by MediaScanner. Files
+ * scanned by MediaScanner appear in the applications used to view media (for example,
+ * Gallery app).
+ * @param mimeType mimetype of the file.
+ * @param path absolute pathname to the file. The file should be world-readable, so that it can
+ * be managed by the Downloads App and any other app that is used to read it (for example,
+ * Gallery app to display the file, if the file contents represent a video/image).
+ * @param length length of the downloaded file
+ * @param showNotification true if a notification is to be sent, false otherwise
+ * @param uri the original HTTP URI of the download
+ * @param referer the HTTP Referer for the download
+ * @return an ID for the download entry added to the downloads app, unique across the system
+ * This ID is used to make future calls related to this download.
+ */
+ public long addCompletedDownload(String title, String description,
+ boolean isMediaScannerScannable, String mimeType, String path, long length,
+ boolean showNotification, Uri uri, Uri referer) {
+ return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
+ length, showNotification, false, uri, referer);
+ }
+
+ /** {@hide} */
+ public long addCompletedDownload(String title, String description,
+ boolean isMediaScannerScannable, String mimeType, String path, long length,
+ boolean showNotification, boolean allowWrite) {
+ return addCompletedDownload(title, description, isMediaScannerScannable, mimeType, path,
+ length, showNotification, allowWrite, null, null);
+ }
+
+ /** {@hide} */
+ public long addCompletedDownload(String title, String description,
+ boolean isMediaScannerScannable, String mimeType, String path, long length,
+ boolean showNotification, boolean allowWrite, Uri uri, Uri referer) {
+ // make sure the input args are non-null/non-zero
+ validateArgumentIsNonEmpty("title", title);
+ validateArgumentIsNonEmpty("description", description);
+ validateArgumentIsNonEmpty("path", path);
+ validateArgumentIsNonEmpty("mimeType", mimeType);
+ if (length < 0) {
+ throw new IllegalArgumentException(" invalid value for param: totalBytes");
+ }
+
+ // if there is already an entry with the given path name in downloads.db, return its id
+ Request request;
+ if (uri != null) {
+ request = new Request(uri);
+ } else {
+ request = new Request(NON_DOWNLOADMANAGER_DOWNLOAD);
+ }
+ request.setTitle(title)
+ .setDescription(description)
+ .setMimeType(mimeType);
+ if (referer != null) {
+ request.addRequestHeader("Referer", referer.toString());
+ }
+ ContentValues values = request.toContentValues(null);
+ values.put(Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD);
+ values.put(Downloads.Impl._DATA, path);
+ values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, length);
+ values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED,
+ (isMediaScannerScannable) ? Request.SCANNABLE_VALUE_YES :
+ Request.SCANNABLE_VALUE_NO);
+ values.put(Downloads.Impl.COLUMN_VISIBILITY, (showNotification) ?
+ Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
+ values.put(Downloads.Impl.COLUMN_ALLOW_WRITE, allowWrite ? 1 : 0);
+ Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
+ if (downloadUri == null) {
+ return -1;
+ }
+ return Long.parseLong(downloadUri.getLastPathSegment());
+ }
+
+ private static final String NON_DOWNLOADMANAGER_DOWNLOAD =
+ "non-dwnldmngr-download-dont-retry2download";
+
+ private static void validateArgumentIsNonEmpty(String paramName, String val) {
+ if (TextUtils.isEmpty(val)) {
+ throw new IllegalArgumentException(paramName + " can't be null");
+ }
+ }
+
+ /**
+ * Get the DownloadProvider URI for the download with the given ID.
+ *
+ * @hide
+ */
+ public Uri getDownloadUri(long id) {
+ return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
+ }
+
+ /**
+ * Get a parameterized SQL WHERE clause to select a bunch of IDs.
+ */
+ static String getWhereClauseForIds(long[] ids) {
+ StringBuilder whereClause = new StringBuilder();
+ whereClause.append("(");
+ for (int i = 0; i < ids.length; i++) {
+ if (i > 0) {
+ whereClause.append("OR ");
+ }
+ whereClause.append(Downloads.Impl._ID);
+ whereClause.append(" = ? ");
+ }
+ whereClause.append(")");
+ return whereClause.toString();
+ }
+
+ /**
+ * Get the selection args for a clause returned by {@link #getWhereClauseForIds(long[])}.
+ */
+ static String[] getWhereArgsForIds(long[] ids) {
+ String[] whereArgs = new String[ids.length];
+ return getWhereArgsForIds(ids, whereArgs);
+ }
+
+ /**
+ * Get selection args for a clause returned by {@link #getWhereClauseForIds(long[])}
+ * and write it to the supplied args array.
+ */
+ static String[] getWhereArgsForIds(long[] ids, String[] args) {
+ assert(args.length >= ids.length);
+ for (int i = 0; i < ids.length; i++) {
+ args[i] = Long.toString(ids[i]);
+ }
+ return args;
+ }
+
+
+ /**
+ * This class wraps a cursor returned by DownloadProvider -- the "underlying cursor" -- and
+ * presents a different set of columns, those defined in the DownloadManager.COLUMN_* constants.
+ * Some columns correspond directly to underlying values while others are computed from
+ * underlying data.
+ */
+ private static class CursorTranslator extends CursorWrapper {
+ private final Uri mBaseUri;
+ private final boolean mAccessFilename;
+
+ public CursorTranslator(Cursor cursor, Uri baseUri, boolean accessFilename) {
+ super(cursor);
+ mBaseUri = baseUri;
+ mAccessFilename = accessFilename;
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ return (int) getLong(columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ if (getColumnName(columnIndex).equals(COLUMN_REASON)) {
+ return getReason(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
+ } else if (getColumnName(columnIndex).equals(COLUMN_STATUS)) {
+ return translateStatus(super.getInt(getColumnIndex(Downloads.Impl.COLUMN_STATUS)));
+ } else {
+ return super.getLong(columnIndex);
+ }
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ final String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case COLUMN_LOCAL_URI:
+ return getLocalUri();
+ case COLUMN_LOCAL_FILENAME:
+ if (!mAccessFilename) {
+ throw new SecurityException(
+ "COLUMN_LOCAL_FILENAME is deprecated;"
+ + " use ContentResolver.openFileDescriptor() instead");
+ }
+ default:
+ return super.getString(columnIndex);
+ }
+ }
+
+ private String getLocalUri() {
+ long destinationType = getLong(getColumnIndex(Downloads.Impl.COLUMN_DESTINATION));
+ if (destinationType == Downloads.Impl.DESTINATION_FILE_URI ||
+ destinationType == Downloads.Impl.DESTINATION_EXTERNAL ||
+ destinationType == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
+ String localPath = super.getString(getColumnIndex(COLUMN_LOCAL_FILENAME));
+ if (localPath == null) {
+ return null;
+ }
+ return Uri.fromFile(new File(localPath)).toString();
+ }
+
+ // return content URI for cache download
+ long downloadId = getLong(getColumnIndex(Downloads.Impl._ID));
+ return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId).toString();
+ }
+
+ private long getReason(int status) {
+ switch (translateStatus(status)) {
+ case STATUS_FAILED:
+ return getErrorCode(status);
+
+ case STATUS_PAUSED:
+ return getPausedReason(status);
+
+ default:
+ return 0; // arbitrary value when status is not an error
+ }
+ }
+
+ private long getPausedReason(int status) {
+ switch (status) {
+ case Downloads.Impl.STATUS_WAITING_TO_RETRY:
+ return PAUSED_WAITING_TO_RETRY;
+
+ case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
+ return PAUSED_WAITING_FOR_NETWORK;
+
+ case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
+ return PAUSED_QUEUED_FOR_WIFI;
+
+ default:
+ return PAUSED_UNKNOWN;
+ }
+ }
+
+ private long getErrorCode(int status) {
+ if ((400 <= status && status < Downloads.Impl.MIN_ARTIFICIAL_ERROR_STATUS)
+ || (500 <= status && status < 600)) {
+ // HTTP status code
+ return status;
+ }
+
+ switch (status) {
+ case Downloads.Impl.STATUS_FILE_ERROR:
+ return ERROR_FILE_ERROR;
+
+ case Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE:
+ case Downloads.Impl.STATUS_UNHANDLED_REDIRECT:
+ return ERROR_UNHANDLED_HTTP_CODE;
+
+ case Downloads.Impl.STATUS_HTTP_DATA_ERROR:
+ return ERROR_HTTP_DATA_ERROR;
+
+ case Downloads.Impl.STATUS_TOO_MANY_REDIRECTS:
+ return ERROR_TOO_MANY_REDIRECTS;
+
+ case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
+ return ERROR_INSUFFICIENT_SPACE;
+
+ case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
+ return ERROR_DEVICE_NOT_FOUND;
+
+ case Downloads.Impl.STATUS_CANNOT_RESUME:
+ return ERROR_CANNOT_RESUME;
+
+ case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
+ return ERROR_FILE_ALREADY_EXISTS;
+
+ default:
+ return ERROR_UNKNOWN;
+ }
+ }
+
+ private int translateStatus(int status) {
+ switch (status) {
+ case Downloads.Impl.STATUS_PENDING:
+ return STATUS_PENDING;
+
+ case Downloads.Impl.STATUS_RUNNING:
+ return STATUS_RUNNING;
+
+ case Downloads.Impl.STATUS_PAUSED_BY_APP:
+ case Downloads.Impl.STATUS_WAITING_TO_RETRY:
+ case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
+ case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
+ return STATUS_PAUSED;
+
+ case Downloads.Impl.STATUS_SUCCESS:
+ return STATUS_SUCCESSFUL;
+
+ default:
+ assert Downloads.Impl.isStatusError(status);
+ return STATUS_FAILED;
+ }
+ }
+ }
+}
diff --git a/android/app/EnterTransitionCoordinator.java b/android/app/EnterTransitionCoordinator.java
new file mode 100644
index 00000000..ab847fd5
--- /dev/null
+++ b/android/app/EnterTransitionCoordinator.java
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.SharedElementCallback.OnSharedElementsReadyListener;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.transition.Transition;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.util.ArrayMap;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.view.OneShotPreDrawListener;
+
+import java.util.ArrayList;
+
+/**
+ * This ActivityTransitionCoordinator is created by the Activity to manage
+ * the enter scene and shared element transfer into the Scene, either during
+ * launch of an Activity or returning from a launched Activity.
+ */
+class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
+ private static final String TAG = "EnterTransitionCoordinator";
+
+ private static final int MIN_ANIMATION_FRAMES = 2;
+
+ private boolean mSharedElementTransitionStarted;
+ private Activity mActivity;
+ private boolean mHasStopped;
+ private boolean mIsCanceled;
+ private ObjectAnimator mBackgroundAnimator;
+ private boolean mIsExitTransitionComplete;
+ private boolean mIsReadyForTransition;
+ private Bundle mSharedElementsBundle;
+ private boolean mWasOpaque;
+ private boolean mAreViewsReady;
+ private boolean mIsViewsTransitionStarted;
+ private Transition mEnterViewsTransition;
+ private OneShotPreDrawListener mViewsReadyListener;
+ private final boolean mIsCrossTask;
+ private Drawable mReplacedBackground;
+
+ public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
+ ArrayList<String> sharedElementNames, boolean isReturning, boolean isCrossTask) {
+ super(activity.getWindow(), sharedElementNames,
+ getListener(activity, isReturning && !isCrossTask), isReturning);
+ mActivity = activity;
+ mIsCrossTask = isCrossTask;
+ setResultReceiver(resultReceiver);
+ prepareEnter();
+ Bundle resultReceiverBundle = new Bundle();
+ resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
+ mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
+ final View decorView = getDecor();
+ if (decorView != null) {
+ final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
+ viewTreeObserver.addOnPreDrawListener(
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (mIsReadyForTransition) {
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.removeOnPreDrawListener(this);
+ } else {
+ decorView.getViewTreeObserver().removeOnPreDrawListener(this);
+ }
+ }
+ return false;
+ }
+ });
+ }
+ }
+
+ boolean isCrossTask() {
+ return mIsCrossTask;
+ }
+
+ public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
+ ArrayList<View> localViews) {
+ boolean remap = false;
+ for (int i = 0; i < localViews.size(); i++) {
+ View view = localViews.get(i);
+ if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
+ || !view.isAttachedToWindow()) {
+ remap = true;
+ break;
+ }
+ }
+ if (remap) {
+ triggerViewsReady(mapNamedElements(accepted, localNames));
+ } else {
+ triggerViewsReady(mapSharedElements(accepted, localViews));
+ }
+ }
+
+ public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
+ triggerViewsReady(mapNamedElements(accepted, localNames));
+ }
+
+ public Transition getEnterViewsTransition() {
+ return mEnterViewsTransition;
+ }
+
+ @Override
+ protected void viewsReady(ArrayMap<String, View> sharedElements) {
+ super.viewsReady(sharedElements);
+ mIsReadyForTransition = true;
+ hideViews(mSharedElements);
+ Transition viewsTransition = getViewsTransition();
+ if (viewsTransition != null && mTransitioningViews != null) {
+ removeExcludedViews(viewsTransition, mTransitioningViews);
+ stripOffscreenViews();
+ hideViews(mTransitioningViews);
+ }
+ if (mIsReturning) {
+ sendSharedElementDestination();
+ } else {
+ moveSharedElementsToOverlay();
+ }
+ if (mSharedElementsBundle != null) {
+ onTakeSharedElements();
+ }
+ }
+
+ private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
+ if (mAreViewsReady) {
+ return;
+ }
+ mAreViewsReady = true;
+ final ViewGroup decor = getDecor();
+ // Ensure the views have been laid out before capturing the views -- we need the epicenter.
+ if (decor == null || (decor.isAttachedToWindow() &&
+ (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
+ viewsReady(sharedElements);
+ } else {
+ mViewsReadyListener = OneShotPreDrawListener.add(decor, () -> {
+ mViewsReadyListener = null;
+ viewsReady(sharedElements);
+ });
+ decor.invalidate();
+ }
+ }
+
+ private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
+ ArrayList<String> localNames) {
+ ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.findNamedViews(sharedElements);
+ }
+ if (accepted != null) {
+ for (int i = 0; i < localNames.size(); i++) {
+ String localName = localNames.get(i);
+ String acceptedName = accepted.get(i);
+ if (localName != null && !localName.equals(acceptedName)) {
+ View view = sharedElements.get(localName);
+ if (view != null) {
+ sharedElements.put(acceptedName, view);
+ }
+ }
+ }
+ }
+ return sharedElements;
+ }
+
+ private void sendSharedElementDestination() {
+ boolean allReady;
+ final View decorView = getDecor();
+ if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
+ allReady = false;
+ } else if (decorView == null) {
+ allReady = true;
+ } else {
+ allReady = !decorView.isLayoutRequested();
+ if (allReady) {
+ for (int i = 0; i < mSharedElements.size(); i++) {
+ if (mSharedElements.get(i).isLayoutRequested()) {
+ allReady = false;
+ break;
+ }
+ }
+ }
+ }
+ if (allReady) {
+ Bundle state = captureSharedElementState();
+ moveSharedElementsToOverlay();
+ mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
+ } else if (decorView != null) {
+ OneShotPreDrawListener.add(decorView, () -> {
+ if (mResultReceiver != null) {
+ Bundle state = captureSharedElementState();
+ moveSharedElementsToOverlay();
+ mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
+ }
+ });
+ }
+ if (allowOverlappingTransitions()) {
+ startEnterTransitionOnly();
+ }
+ }
+
+ private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
+ return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ switch (resultCode) {
+ case MSG_TAKE_SHARED_ELEMENTS:
+ if (!mIsCanceled) {
+ mSharedElementsBundle = resultData;
+ onTakeSharedElements();
+ }
+ break;
+ case MSG_EXIT_TRANSITION_COMPLETE:
+ if (!mIsCanceled) {
+ mIsExitTransitionComplete = true;
+ if (mSharedElementTransitionStarted) {
+ onRemoteExitTransitionComplete();
+ }
+ }
+ break;
+ case MSG_CANCEL:
+ cancel();
+ break;
+ }
+ }
+
+ public boolean isWaitingForRemoteExit() {
+ return mIsReturning && mResultReceiver != null;
+ }
+
+ /**
+ * This is called onResume. If an Activity is resuming and the transitions
+ * haven't started yet, force the views to appear. This is likely to be
+ * caused by the top Activity finishing before the transitions started.
+ * In that case, we can finish any transition that was started, but we
+ * should cancel any pending transition and just bring those Views visible.
+ */
+ public void forceViewsToAppear() {
+ if (!mIsReturning) {
+ return;
+ }
+ if (!mIsReadyForTransition) {
+ mIsReadyForTransition = true;
+ final ViewGroup decor = getDecor();
+ if (decor != null && mViewsReadyListener != null) {
+ mViewsReadyListener.removeListener();
+ mViewsReadyListener = null;
+ }
+ showViews(mTransitioningViews, true);
+ setTransitioningViewsVisiblity(View.VISIBLE, true);
+ mSharedElements.clear();
+ mAllSharedElementNames.clear();
+ mTransitioningViews.clear();
+ mIsReadyForTransition = true;
+ viewsTransitionComplete();
+ sharedElementTransitionComplete();
+ } else {
+ if (!mSharedElementTransitionStarted) {
+ moveSharedElementsFromOverlay();
+ mSharedElementTransitionStarted = true;
+ showViews(mSharedElements, true);
+ mSharedElements.clear();
+ sharedElementTransitionComplete();
+ }
+ if (!mIsViewsTransitionStarted) {
+ mIsViewsTransitionStarted = true;
+ showViews(mTransitioningViews, true);
+ setTransitioningViewsVisiblity(View.VISIBLE, true);
+ mTransitioningViews.clear();
+ viewsTransitionComplete();
+ }
+ cancelPendingTransitions();
+ }
+ mAreViewsReady = true;
+ if (mResultReceiver != null) {
+ mResultReceiver.send(MSG_CANCEL, null);
+ mResultReceiver = null;
+ }
+ }
+
+ private void cancel() {
+ if (!mIsCanceled) {
+ mIsCanceled = true;
+ if (getViewsTransition() == null || mIsViewsTransitionStarted) {
+ showViews(mSharedElements, true);
+ } else if (mTransitioningViews != null) {
+ mTransitioningViews.addAll(mSharedElements);
+ }
+ moveSharedElementsFromOverlay();
+ mSharedElementNames.clear();
+ mSharedElements.clear();
+ mAllSharedElementNames.clear();
+ startSharedElementTransition(null);
+ onRemoteExitTransitionComplete();
+ }
+ }
+
+ public boolean isReturning() {
+ return mIsReturning;
+ }
+
+ protected void prepareEnter() {
+ ViewGroup decorView = getDecor();
+ if (mActivity == null || decorView == null) {
+ return;
+ }
+ if (!isCrossTask()) {
+ mActivity.overridePendingTransition(0, 0);
+ }
+ if (!mIsReturning) {
+ mWasOpaque = mActivity.convertToTranslucent(null, null);
+ Drawable background = decorView.getBackground();
+ if (background == null) {
+ background = new ColorDrawable(Color.TRANSPARENT);
+ mReplacedBackground = background;
+ } else {
+ getWindow().setBackgroundDrawable(null);
+ background = background.mutate();
+ background.setAlpha(0);
+ }
+ getWindow().setBackgroundDrawable(background);
+ } else {
+ mActivity = null; // all done with it now.
+ }
+ }
+
+ @Override
+ protected Transition getViewsTransition() {
+ Window window = getWindow();
+ if (window == null) {
+ return null;
+ }
+ if (mIsReturning) {
+ return window.getReenterTransition();
+ } else {
+ return window.getEnterTransition();
+ }
+ }
+
+ protected Transition getSharedElementTransition() {
+ Window window = getWindow();
+ if (window == null) {
+ return null;
+ }
+ if (mIsReturning) {
+ return window.getSharedElementReenterTransition();
+ } else {
+ return window.getSharedElementEnterTransition();
+ }
+ }
+
+ private void startSharedElementTransition(Bundle sharedElementState) {
+ ViewGroup decorView = getDecor();
+ if (decorView == null) {
+ return;
+ }
+ // Remove rejected shared elements
+ ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
+ rejectedNames.removeAll(mSharedElementNames);
+ ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
+ if (mListener != null) {
+ mListener.onRejectSharedElements(rejectedSnapshots);
+ }
+ removeNullViews(rejectedSnapshots);
+ startRejectedAnimations(rejectedSnapshots);
+
+ // Now start shared element transition
+ ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
+ mSharedElementNames);
+ showViews(mSharedElements, true);
+ scheduleSetSharedElementEnd(sharedElementSnapshots);
+ ArrayList<SharedElementOriginalState> originalImageViewState =
+ setSharedElementState(sharedElementState, sharedElementSnapshots);
+ requestLayoutForSharedElements();
+
+ boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
+ boolean startSharedElementTransition = true;
+ setGhostVisibility(View.INVISIBLE);
+ scheduleGhostVisibilityChange(View.INVISIBLE);
+ pauseInput();
+ Transition transition = beginTransition(decorView, startEnterTransition,
+ startSharedElementTransition);
+ scheduleGhostVisibilityChange(View.VISIBLE);
+ setGhostVisibility(View.VISIBLE);
+
+ if (startEnterTransition) {
+ startEnterTransition(transition);
+ }
+
+ setOriginalSharedElementState(mSharedElements, originalImageViewState);
+
+ if (mResultReceiver != null) {
+ // We can't trust that the view will disappear on the same frame that the shared
+ // element appears here. Assure that we get at least 2 frames for double-buffering.
+ decorView.postOnAnimation(new Runnable() {
+ int mAnimations;
+
+ @Override
+ public void run() {
+ if (mAnimations++ < MIN_ANIMATION_FRAMES) {
+ View decorView = getDecor();
+ if (decorView != null) {
+ decorView.postOnAnimation(this);
+ }
+ } else if (mResultReceiver != null) {
+ mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
+ mResultReceiver = null; // all done sending messages.
+ }
+ }
+ });
+ }
+ }
+
+ private static void removeNullViews(ArrayList<View> views) {
+ if (views != null) {
+ for (int i = views.size() - 1; i >= 0; i--) {
+ if (views.get(i) == null) {
+ views.remove(i);
+ }
+ }
+ }
+ }
+
+ private void onTakeSharedElements() {
+ if (!mIsReadyForTransition || mSharedElementsBundle == null) {
+ return;
+ }
+ final Bundle sharedElementState = mSharedElementsBundle;
+ mSharedElementsBundle = null;
+ OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
+ @Override
+ public void onSharedElementsReady() {
+ final View decorView = getDecor();
+ if (decorView != null) {
+ OneShotPreDrawListener.add(decorView, false, () -> {
+ startTransition(() -> {
+ startSharedElementTransition(sharedElementState);
+ });
+ });
+ decorView.invalidate();
+ }
+ }
+ };
+ if (mListener == null) {
+ listener.onSharedElementsReady();
+ } else {
+ mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
+ }
+ }
+
+ private void requestLayoutForSharedElements() {
+ int numSharedElements = mSharedElements.size();
+ for (int i = 0; i < numSharedElements; i++) {
+ mSharedElements.get(i).requestLayout();
+ }
+ }
+
+ private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
+ boolean startSharedElementTransition) {
+ Transition sharedElementTransition = null;
+ if (startSharedElementTransition) {
+ if (!mSharedElementNames.isEmpty()) {
+ sharedElementTransition = configureTransition(getSharedElementTransition(), false);
+ }
+ if (sharedElementTransition == null) {
+ sharedElementTransitionStarted();
+ sharedElementTransitionComplete();
+ } else {
+ sharedElementTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ sharedElementTransitionStarted();
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ sharedElementTransitionComplete();
+ }
+ });
+ }
+ }
+ Transition viewsTransition = null;
+ if (startEnterTransition) {
+ mIsViewsTransitionStarted = true;
+ if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
+ viewsTransition = configureTransition(getViewsTransition(), true);
+ }
+ if (viewsTransition == null) {
+ viewsTransitionComplete();
+ } else {
+ final ArrayList<View> transitioningViews = mTransitioningViews;
+ viewsTransition.addListener(new ContinueTransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ mEnterViewsTransition = transition;
+ if (transitioningViews != null) {
+ showViews(transitioningViews, false);
+ }
+ super.onTransitionStart(transition);
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ mEnterViewsTransition = null;
+ transition.removeListener(this);
+ viewsTransitionComplete();
+ super.onTransitionEnd(transition);
+ }
+ });
+ }
+ }
+
+ Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
+ if (transition != null) {
+ transition.addListener(new ContinueTransitionListener());
+ if (startEnterTransition) {
+ setTransitioningViewsVisiblity(View.INVISIBLE, false);
+ }
+ TransitionManager.beginDelayedTransition(decorView, transition);
+ if (startEnterTransition) {
+ setTransitioningViewsVisiblity(View.VISIBLE, false);
+ }
+ decorView.invalidate();
+ } else {
+ transitionStarted();
+ }
+ return transition;
+ }
+
+ @Override
+ protected void onTransitionsComplete() {
+ moveSharedElementsFromOverlay();
+ final ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+
+ Window window = getWindow();
+ if (window != null && mReplacedBackground == decorView.getBackground()) {
+ window.setBackgroundDrawable(null);
+ }
+ }
+ }
+
+ private void sharedElementTransitionStarted() {
+ mSharedElementTransitionStarted = true;
+ if (mIsExitTransitionComplete) {
+ send(MSG_EXIT_TRANSITION_COMPLETE, null);
+ }
+ }
+
+ private void startEnterTransition(Transition transition) {
+ ViewGroup decorView = getDecor();
+ if (!mIsReturning && decorView != null) {
+ Drawable background = decorView.getBackground();
+ if (background != null) {
+ background = background.mutate();
+ getWindow().setBackgroundDrawable(background);
+ mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
+ mBackgroundAnimator.setDuration(getFadeDuration());
+ mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ makeOpaque();
+ backgroundAnimatorComplete();
+ }
+ });
+ mBackgroundAnimator.start();
+ } else if (transition != null) {
+ transition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ makeOpaque();
+ }
+ });
+ backgroundAnimatorComplete();
+ } else {
+ makeOpaque();
+ backgroundAnimatorComplete();
+ }
+ } else {
+ backgroundAnimatorComplete();
+ }
+ }
+
+ public void stop() {
+ // Restore the background to its previous state since the
+ // Activity is stopping.
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.end();
+ mBackgroundAnimator = null;
+ } else if (mWasOpaque) {
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ Drawable drawable = decorView.getBackground();
+ if (drawable != null) {
+ drawable.setAlpha(1);
+ }
+ }
+ }
+ makeOpaque();
+ mIsCanceled = true;
+ mResultReceiver = null;
+ mActivity = null;
+ moveSharedElementsFromOverlay();
+ if (mTransitioningViews != null) {
+ showViews(mTransitioningViews, true);
+ setTransitioningViewsVisiblity(View.VISIBLE, true);
+ }
+ showViews(mSharedElements, true);
+ clearState();
+ }
+
+ /**
+ * Cancels the enter transition.
+ * @return True if the enter transition is still pending capturing the target state. If so,
+ * any transition started on the decor will do nothing.
+ */
+ public boolean cancelEnter() {
+ setGhostVisibility(View.INVISIBLE);
+ mHasStopped = true;
+ mIsCanceled = true;
+ clearState();
+ return super.cancelPendingTransitions();
+ }
+
+ @Override
+ protected void clearState() {
+ mSharedElementsBundle = null;
+ mEnterViewsTransition = null;
+ mResultReceiver = null;
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.cancel();
+ mBackgroundAnimator = null;
+ }
+ super.clearState();
+ }
+
+ private void makeOpaque() {
+ if (!mHasStopped && mActivity != null) {
+ if (mWasOpaque) {
+ mActivity.convertFromTranslucent();
+ }
+ mActivity = null;
+ }
+ }
+
+ private boolean allowOverlappingTransitions() {
+ return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
+ : getWindow().getAllowEnterTransitionOverlap();
+ }
+
+ private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
+ if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
+ return;
+ }
+ final ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ ViewGroupOverlay overlay = decorView.getOverlay();
+ ObjectAnimator animator = null;
+ int numRejected = rejectedSnapshots.size();
+ for (int i = 0; i < numRejected; i++) {
+ View snapshot = rejectedSnapshots.get(i);
+ overlay.add(snapshot);
+ animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
+ animator.start();
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ViewGroupOverlay overlay = decorView.getOverlay();
+ int numRejected = rejectedSnapshots.size();
+ for (int i = 0; i < numRejected; i++) {
+ overlay.remove(rejectedSnapshots.get(i));
+ }
+ }
+ });
+ }
+ }
+
+ protected void onRemoteExitTransitionComplete() {
+ if (!allowOverlappingTransitions()) {
+ startEnterTransitionOnly();
+ }
+ }
+
+ private void startEnterTransitionOnly() {
+ startTransition(new Runnable() {
+ @Override
+ public void run() {
+ boolean startEnterTransition = true;
+ boolean startSharedElementTransition = false;
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ Transition transition = beginTransition(decorView, startEnterTransition,
+ startSharedElementTransition);
+ startEnterTransition(transition);
+ }
+ }
+ });
+ }
+}
diff --git a/android/app/EphemeralResolverService.java b/android/app/EphemeralResolverService.java
new file mode 100644
index 00000000..427a0386
--- /dev/null
+++ b/android/app/EphemeralResolverService.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.InstantAppResolverService.InstantAppResolutionCallback;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.EphemeralResolveInfo;
+import android.content.pm.InstantAppResolveInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Base class for implementing the resolver service.
+ * @hide
+ * @removed
+ * @deprecated use InstantAppResolverService instead
+ */
+@Deprecated
+@SystemApi
+public abstract class EphemeralResolverService extends InstantAppResolverService {
+ private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE;
+ private static final String TAG = "PackageManager";
+
+ /**
+ * Called to retrieve resolve info for ephemeral applications.
+ *
+ * @param digestPrefix The hash prefix of the ephemeral's domain.
+ * @param prefixMask A mask that was applied to each digest prefix. This should
+ * be used when comparing against the digest prefixes as all bits might
+ * not be set.
+ * @deprecated use {@link #onGetEphemeralResolveInfo(int[])} instead
+ */
+ @Deprecated
+ public abstract List<EphemeralResolveInfo> onEphemeralResolveInfoList(
+ int digestPrefix[], int prefix);
+
+ /**
+ * Called to retrieve resolve info for ephemeral applications.
+ *
+ * @param digestPrefix The hash prefix of the ephemeral's domain.
+ */
+ public List<EphemeralResolveInfo> onGetEphemeralResolveInfo(int digestPrefix[]) {
+ return onEphemeralResolveInfoList(digestPrefix, 0xFFFFF000);
+ }
+
+ /**
+ * Called to retrieve intent filters for ephemeral applications.
+ *
+ * @param hostName The name of the host to get intent filters for.
+ */
+ public EphemeralResolveInfo onGetEphemeralIntentFilter(String hostName) {
+ throw new IllegalStateException("Must define");
+ }
+
+ @Override
+ public Looper getLooper() {
+ return super.getLooper();
+ }
+
+ @Override
+ void _onGetInstantAppResolveInfo(int[] digestPrefix, String token,
+ InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Log.d(TAG, "Legacy resolver; getInstantAppResolveInfo;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ final List<EphemeralResolveInfo> response = onGetEphemeralResolveInfo(digestPrefix);
+ final int responseSize = response == null ? 0 : response.size();
+ final List<InstantAppResolveInfo> resultList = new ArrayList<>(responseSize);
+ for (int i = 0; i < responseSize; i++) {
+ resultList.add(response.get(i).getInstantAppResolveInfo());
+ }
+ callback.onInstantAppResolveInfo(resultList);
+ }
+
+ @Override
+ void _onGetInstantAppIntentFilter(int[] digestPrefix, String token,
+ String hostName, InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Log.d(TAG, "Legacy resolver; getInstantAppIntentFilter;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ final EphemeralResolveInfo response = onGetEphemeralIntentFilter(hostName);
+ final List<InstantAppResolveInfo> resultList = new ArrayList<>(1);
+ resultList.add(response.getInstantAppResolveInfo());
+ callback.onInstantAppResolveInfo(resultList);
+ }
+}
diff --git a/android/app/ExitTransitionCoordinator.java b/android/app/ExitTransitionCoordinator.java
new file mode 100644
index 00000000..df31da91
--- /dev/null
+++ b/android/app/ExitTransitionCoordinator.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.SharedElementCallback.OnSharedElementsReadyListener;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.transition.Transition;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import com.android.internal.view.OneShotPreDrawListener;
+
+import java.util.ArrayList;
+
+/**
+ * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
+ * to govern the exit of the Scene and the shared elements when calling an Activity as well as
+ * the reentry of the Scene when coming back from the called Activity.
+ */
+class ExitTransitionCoordinator extends ActivityTransitionCoordinator {
+ private static final String TAG = "ExitTransitionCoordinator";
+ private static final long MAX_WAIT_MS = 1000;
+
+ private Bundle mSharedElementBundle;
+ private boolean mExitNotified;
+ private boolean mSharedElementNotified;
+ private Activity mActivity;
+ private boolean mIsBackgroundReady;
+ private boolean mIsCanceled;
+ private Handler mHandler;
+ private ObjectAnimator mBackgroundAnimator;
+ private boolean mIsHidden;
+ private Bundle mExitSharedElementBundle;
+ private boolean mIsExitStarted;
+ private boolean mSharedElementsHidden;
+ private HideSharedElementsCallback mHideSharedElementsCallback;
+
+ public ExitTransitionCoordinator(Activity activity, Window window,
+ SharedElementCallback listener, ArrayList<String> names,
+ ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
+ super(window, names, listener, isReturning);
+ viewsReady(mapSharedElements(accepted, mapped));
+ stripOffscreenViews();
+ mIsBackgroundReady = !isReturning;
+ mActivity = activity;
+ }
+
+ void setHideSharedElementsCallback(HideSharedElementsCallback callback) {
+ mHideSharedElementsCallback = callback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ switch (resultCode) {
+ case MSG_SET_REMOTE_RECEIVER:
+ stopCancel();
+ mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER);
+ if (mIsCanceled) {
+ mResultReceiver.send(MSG_CANCEL, null);
+ mResultReceiver = null;
+ } else {
+ notifyComplete();
+ }
+ break;
+ case MSG_HIDE_SHARED_ELEMENTS:
+ stopCancel();
+ if (!mIsCanceled) {
+ hideSharedElements();
+ }
+ break;
+ case MSG_START_EXIT_TRANSITION:
+ mHandler.removeMessages(MSG_CANCEL);
+ startExit();
+ break;
+ case MSG_SHARED_ELEMENT_DESTINATION:
+ mExitSharedElementBundle = resultData;
+ sharedElementExitBack();
+ break;
+ case MSG_CANCEL:
+ mIsCanceled = true;
+ finish();
+ break;
+ }
+ }
+
+ private void stopCancel() {
+ if (mHandler != null) {
+ mHandler.removeMessages(MSG_CANCEL);
+ }
+ }
+
+ private void delayCancel() {
+ if (mHandler != null) {
+ mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS);
+ }
+ }
+
+ public void resetViews() {
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ TransitionManager.endTransitions(decorView);
+ }
+ if (mTransitioningViews != null) {
+ showViews(mTransitioningViews, true);
+ setTransitioningViewsVisiblity(View.VISIBLE, true);
+ }
+ showViews(mSharedElements, true);
+ mIsHidden = true;
+ if (!mIsReturning && decorView != null) {
+ decorView.suppressLayout(false);
+ }
+ moveSharedElementsFromOverlay();
+ clearState();
+ }
+
+ private void sharedElementExitBack() {
+ final ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.suppressLayout(true);
+ }
+ if (decorView != null && mExitSharedElementBundle != null &&
+ !mExitSharedElementBundle.isEmpty() &&
+ !mSharedElements.isEmpty() && getSharedElementTransition() != null) {
+ startTransition(new Runnable() {
+ public void run() {
+ startSharedElementExit(decorView);
+ }
+ });
+ } else {
+ sharedElementTransitionComplete();
+ }
+ }
+
+ private void startSharedElementExit(final ViewGroup decorView) {
+ Transition transition = getSharedElementExitTransition();
+ transition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ if (isViewsTransitionComplete()) {
+ delayCancel();
+ }
+ }
+ });
+ final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
+ mSharedElementNames);
+ OneShotPreDrawListener.add(decorView, () -> {
+ setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
+ });
+ setGhostVisibility(View.INVISIBLE);
+ scheduleGhostVisibilityChange(View.INVISIBLE);
+ if (mListener != null) {
+ mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
+ sharedElementSnapshots);
+ }
+ TransitionManager.beginDelayedTransition(decorView, transition);
+ scheduleGhostVisibilityChange(View.VISIBLE);
+ setGhostVisibility(View.VISIBLE);
+ decorView.invalidate();
+ }
+
+ private void hideSharedElements() {
+ moveSharedElementsFromOverlay();
+ if (mHideSharedElementsCallback != null) {
+ mHideSharedElementsCallback.hideSharedElements();
+ }
+ if (!mIsHidden) {
+ hideViews(mSharedElements);
+ }
+ mSharedElementsHidden = true;
+ finishIfNecessary();
+ }
+
+ public void startExit() {
+ if (!mIsExitStarted) {
+ backgroundAnimatorComplete();
+ mIsExitStarted = true;
+ pauseInput();
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.suppressLayout(true);
+ }
+ moveSharedElementsToOverlay();
+ startTransition(new Runnable() {
+ @Override
+ public void run() {
+ if (mActivity != null) {
+ beginTransitions();
+ } else {
+ startExitTransition();
+ }
+ }
+ });
+ }
+ }
+
+ public void startExit(int resultCode, Intent data) {
+ if (!mIsExitStarted) {
+ mIsExitStarted = true;
+ pauseInput();
+ ViewGroup decorView = getDecor();
+ if (decorView != null) {
+ decorView.suppressLayout(true);
+ }
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ mIsCanceled = true;
+ finish();
+ }
+ };
+ delayCancel();
+ moveSharedElementsToOverlay();
+ if (decorView != null && decorView.getBackground() == null) {
+ getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ }
+ final boolean targetsM = decorView == null || decorView.getContext()
+ .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
+ ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
+ mAllSharedElementNames;
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this,
+ sharedElementNames, resultCode, data);
+ mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() {
+ @Override
+ public void onTranslucentConversionComplete(boolean drawComplete) {
+ if (!mIsCanceled) {
+ fadeOutBackground();
+ }
+ }
+ }, options);
+ startTransition(new Runnable() {
+ @Override
+ public void run() {
+ startExitTransition();
+ }
+ });
+ }
+ }
+
+ public void stop() {
+ if (mIsReturning && mActivity != null) {
+ // Override the previous ActivityOptions. We don't want the
+ // activity to have options since we're essentially canceling the
+ // transition and finishing right now.
+ mActivity.convertToTranslucent(null, null);
+ finish();
+ }
+ }
+
+ private void startExitTransition() {
+ Transition transition = getExitTransition();
+ ViewGroup decorView = getDecor();
+ if (transition != null && decorView != null && mTransitioningViews != null) {
+ setTransitioningViewsVisiblity(View.VISIBLE, false);
+ TransitionManager.beginDelayedTransition(decorView, transition);
+ setTransitioningViewsVisiblity(View.INVISIBLE, false);
+ decorView.invalidate();
+ } else {
+ transitionStarted();
+ }
+ }
+
+ private void fadeOutBackground() {
+ if (mBackgroundAnimator == null) {
+ ViewGroup decor = getDecor();
+ Drawable background;
+ if (decor != null && (background = decor.getBackground()) != null) {
+ background = background.mutate();
+ getWindow().setBackgroundDrawable(background);
+ mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
+ mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBackgroundAnimator = null;
+ if (!mIsCanceled) {
+ mIsBackgroundReady = true;
+ notifyComplete();
+ }
+ backgroundAnimatorComplete();
+ }
+ });
+ mBackgroundAnimator.setDuration(getFadeDuration());
+ mBackgroundAnimator.start();
+ } else {
+ backgroundAnimatorComplete();
+ mIsBackgroundReady = true;
+ }
+ }
+ }
+
+ private Transition getExitTransition() {
+ Transition viewsTransition = null;
+ if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
+ viewsTransition = configureTransition(getViewsTransition(), true);
+ removeExcludedViews(viewsTransition, mTransitioningViews);
+ if (mTransitioningViews.isEmpty()) {
+ viewsTransition = null;
+ }
+ }
+ if (viewsTransition == null) {
+ viewsTransitionComplete();
+ } else {
+ final ArrayList<View> transitioningViews = mTransitioningViews;
+ viewsTransition.addListener(new ContinueTransitionListener() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ viewsTransitionComplete();
+ if (mIsHidden && transitioningViews != null) {
+ showViews(transitioningViews, true);
+ setTransitioningViewsVisiblity(View.VISIBLE, true);
+ }
+ if (mSharedElementBundle != null) {
+ delayCancel();
+ }
+ super.onTransitionEnd(transition);
+ }
+ });
+ }
+ return viewsTransition;
+ }
+
+ private Transition getSharedElementExitTransition() {
+ Transition sharedElementTransition = null;
+ if (!mSharedElements.isEmpty()) {
+ sharedElementTransition = configureTransition(getSharedElementTransition(), false);
+ }
+ if (sharedElementTransition == null) {
+ sharedElementTransitionComplete();
+ } else {
+ sharedElementTransition.addListener(new ContinueTransitionListener() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ sharedElementTransitionComplete();
+ if (mIsHidden) {
+ showViews(mSharedElements, true);
+ }
+ super.onTransitionEnd(transition);
+ }
+ });
+ mSharedElements.get(0).invalidate();
+ }
+ return sharedElementTransition;
+ }
+
+ private void beginTransitions() {
+ Transition sharedElementTransition = getSharedElementExitTransition();
+ Transition viewsTransition = getExitTransition();
+
+ Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
+ ViewGroup decorView = getDecor();
+ if (transition != null && decorView != null) {
+ setGhostVisibility(View.INVISIBLE);
+ scheduleGhostVisibilityChange(View.INVISIBLE);
+ if (viewsTransition != null) {
+ setTransitioningViewsVisiblity(View.VISIBLE, false);
+ }
+ TransitionManager.beginDelayedTransition(decorView, transition);
+ scheduleGhostVisibilityChange(View.VISIBLE);
+ setGhostVisibility(View.VISIBLE);
+ if (viewsTransition != null) {
+ setTransitioningViewsVisiblity(View.INVISIBLE, false);
+ }
+ decorView.invalidate();
+ } else {
+ transitionStarted();
+ }
+ }
+
+ protected boolean isReadyToNotify() {
+ return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
+ }
+
+ @Override
+ protected void sharedElementTransitionComplete() {
+ mSharedElementBundle = mExitSharedElementBundle == null
+ ? captureSharedElementState() : captureExitSharedElementsState();
+ super.sharedElementTransitionComplete();
+ }
+
+ private Bundle captureExitSharedElementsState() {
+ Bundle bundle = new Bundle();
+ RectF bounds = new RectF();
+ Matrix matrix = new Matrix();
+ for (int i = 0; i < mSharedElements.size(); i++) {
+ String name = mSharedElementNames.get(i);
+ Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
+ if (sharedElementState != null) {
+ bundle.putBundle(name, sharedElementState);
+ } else {
+ View view = mSharedElements.get(i);
+ captureSharedElementState(view, name, bundle, matrix, bounds);
+ }
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onTransitionsComplete() {
+ notifyComplete();
+ }
+
+ protected void notifyComplete() {
+ if (isReadyToNotify()) {
+ if (!mSharedElementNotified) {
+ mSharedElementNotified = true;
+ delayCancel();
+ if (mListener == null) {
+ mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
+ notifyExitComplete();
+ } else {
+ final ResultReceiver resultReceiver = mResultReceiver;
+ final Bundle sharedElementBundle = mSharedElementBundle;
+ mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
+ new OnSharedElementsReadyListener() {
+ @Override
+ public void onSharedElementsReady() {
+ resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
+ sharedElementBundle);
+ notifyExitComplete();
+ }
+ });
+ }
+ } else {
+ notifyExitComplete();
+ }
+ }
+ }
+
+ private void notifyExitComplete() {
+ if (!mExitNotified && isViewsTransitionComplete()) {
+ mExitNotified = true;
+ mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
+ mResultReceiver = null; // done talking
+ ViewGroup decorView = getDecor();
+ if (!mIsReturning && decorView != null) {
+ decorView.suppressLayout(false);
+ }
+ finishIfNecessary();
+ }
+ }
+
+ private void finishIfNecessary() {
+ if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() ||
+ mSharedElementsHidden)) {
+ finish();
+ }
+ if (!mIsReturning && mExitNotified) {
+ mActivity = null; // don't need it anymore
+ }
+ }
+
+ private void finish() {
+ stopCancel();
+ if (mActivity != null) {
+ mActivity.mActivityTransitionState.clear();
+ mActivity.finish();
+ mActivity.overridePendingTransition(0, 0);
+ mActivity = null;
+ }
+ // Clear the state so that we can't hold any references accidentally and leak memory.
+ clearState();
+ }
+
+ @Override
+ protected void clearState() {
+ mHandler = null;
+ mSharedElementBundle = null;
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.cancel();
+ mBackgroundAnimator = null;
+ }
+ mExitSharedElementBundle = null;
+ super.clearState();
+ }
+
+ @Override
+ protected boolean moveSharedElementWithParent() {
+ return !mIsReturning;
+ }
+
+ @Override
+ protected Transition getViewsTransition() {
+ if (mIsReturning) {
+ return getWindow().getReturnTransition();
+ } else {
+ return getWindow().getExitTransition();
+ }
+ }
+
+ protected Transition getSharedElementTransition() {
+ if (mIsReturning) {
+ return getWindow().getSharedElementReturnTransition();
+ } else {
+ return getWindow().getSharedElementExitTransition();
+ }
+ }
+
+ interface HideSharedElementsCallback {
+ void hideSharedElements();
+ }
+}
diff --git a/android/app/ExpandableListActivity.java b/android/app/ExpandableListActivity.java
new file mode 100644
index 00000000..e08f25a1
--- /dev/null
+++ b/android/app/ExpandableListActivity.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import java.util.List;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.SimpleCursorTreeAdapter;
+import android.widget.SimpleExpandableListAdapter;
+
+import java.util.Map;
+
+/**
+ * An activity that displays an expandable list of items by binding to a data
+ * source implementing the ExpandableListAdapter, and exposes event handlers
+ * when the user selects an item.
+ * <p>
+ * ExpandableListActivity hosts a
+ * {@link android.widget.ExpandableListView ExpandableListView} object that can
+ * be bound to different data sources that provide a two-levels of data (the
+ * top-level is group, and below each group are children). Binding, screen
+ * layout, and row layout are discussed in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ExpandableListActivity has a default layout that consists of a single,
+ * full-screen, centered expandable list. However, if you desire, you can
+ * customize the screen layout by setting your own view layout with
+ * setContentView() in onCreate(). To do this, your own view MUST contain an
+ * ExpandableListView object with the id "@android:id/list" (or
+ * {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your custom view can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:empty". Note that when an empty view is present, the expandable
+ * list view will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom screen layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:paddingLeft=&quot;8dp&quot;
+ * android:paddingRight=&quot;8dp&quot;&gt;
+ *
+ * &lt;ExpandableListView android:id=&quot;@id/android:list&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@id/android:empty&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * The {@link ExpandableListAdapter} set in the {@link ExpandableListActivity}
+ * via {@link #setListAdapter(ExpandableListAdapter)} provides the {@link View}s
+ * for each row. This adapter has separate methods for providing the group
+ * {@link View}s and child {@link View}s. There are a couple provided
+ * {@link ExpandableListAdapter}s that simplify use of adapters:
+ * {@link SimpleCursorTreeAdapter} and {@link SimpleExpandableListAdapter}.
+ * <p>
+ * With these, you can specify the layout of individual rows for groups and
+ * children in the list. These constructor takes a few parameters that specify
+ * layout resources for groups and children. It also has additional parameters
+ * that let you specify which data field to associate with which object in the
+ * row layout resource. The {@link SimpleCursorTreeAdapter} fetches data from
+ * {@link Cursor}s and the {@link SimpleExpandableListAdapter} fetches data
+ * from {@link List}s of {@link Map}s.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text1&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text2&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ExpandableListActivity's ExpandableListView object to data using
+ * a class that implements the
+ * {@link android.widget.ExpandableListAdapter ExpandableListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleExpandableListAdapter SimpleExpandableListAdapter}
+ * for static data (Maps), and
+ * {@link android.widget.SimpleCursorTreeAdapter SimpleCursorTreeAdapter} for
+ * Cursor query results.
+ * </p>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ExpandableListView
+ */
+public class ExpandableListActivity extends Activity implements
+ OnCreateContextMenuListener,
+ ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener,
+ ExpandableListView.OnGroupExpandListener {
+ ExpandableListAdapter mAdapter;
+ ExpandableListView mList;
+ boolean mFinishedStart = false;
+
+ /**
+ * Override this to populate the context menu when an item is long pressed. menuInfo
+ * will contain an {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo}
+ * whose packedPosition is a packed position
+ * that should be used with {@link ExpandableListView#getPackedPositionType(long)} and
+ * the other similar methods.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a child has been clicked.
+ * <p>
+ * {@inheritDoc}
+ */
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ return false;
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been collapsed.
+ */
+ public void onGroupCollapse(int groupPosition) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been expanded.
+ */
+ public void onGroupExpand(int groupPosition) {
+ }
+
+ /**
+ * Ensures the expandable list view has been created before Activity restores all
+ * of the view states.
+ *
+ *@see Activity#onRestoreInstanceState(Bundle)
+ */
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ ensureList();
+ super.onRestoreInstanceState(state);
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ View emptyView = findViewById(com.android.internal.R.id.empty);
+ mList = (ExpandableListView)findViewById(com.android.internal.R.id.list);
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ExpandableListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mList.setEmptyView(emptyView);
+ }
+ mList.setOnChildClickListener(this);
+ mList.setOnGroupExpandListener(this);
+ mList.setOnGroupCollapseListener(this);
+
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mFinishedStart = true;
+ }
+
+ /**
+ * Provide the adapter for the expandable list.
+ */
+ public void setListAdapter(ExpandableListAdapter adapter) {
+ synchronized (this) {
+ ensureList();
+ mAdapter = adapter;
+ mList.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Get the activity's expandable list view widget. This can be used to get the selection,
+ * set the selection, and many other useful functions.
+ *
+ * @see ExpandableListView
+ */
+ public ExpandableListView getExpandableListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * Get the ExpandableListAdapter associated with this activity's
+ * ExpandableListView.
+ */
+ public ExpandableListAdapter getExpandableListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ setContentView(com.android.internal.R.layout.expandable_list_content);
+ }
+
+ /**
+ * Gets the ID of the currently selected group or child.
+ *
+ * @return The ID of the currently selected group or child.
+ */
+ public long getSelectedId() {
+ return mList.getSelectedId();
+ }
+
+ /**
+ * Gets the position (in packed position representation) of the currently
+ * selected group or child. Use
+ * {@link ExpandableListView#getPackedPositionType},
+ * {@link ExpandableListView#getPackedPositionGroup}, and
+ * {@link ExpandableListView#getPackedPositionChild} to unpack the returned
+ * packed position.
+ *
+ * @return A packed position representation containing the currently
+ * selected group or child's position and type.
+ */
+ public long getSelectedPosition() {
+ return mList.getSelectedPosition();
+ }
+
+ /**
+ * Sets the selection to the specified child. If the child is in a collapsed
+ * group, the group will only be expanded and child subsequently selected if
+ * shouldExpandGroup is set to true, otherwise the method will return false.
+ *
+ * @param groupPosition The position of the group that contains the child.
+ * @param childPosition The position of the child within the group.
+ * @param shouldExpandGroup Whether the child's group should be expanded if
+ * it is collapsed.
+ * @return Whether the selection was successfully set on the child.
+ */
+ public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+ return mList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup);
+ }
+
+ /**
+ * Sets the selection to the specified group.
+ * @param groupPosition The position of the group that should be selected.
+ */
+ public void setSelectedGroup(int groupPosition) {
+ mList.setSelectedGroup(groupPosition);
+ }
+
+}
+
diff --git a/android/app/Fragment.java b/android/app/Fragment.java
new file mode 100644
index 00000000..93773454
--- /dev/null
+++ b/android/app/Fragment.java
@@ -0,0 +1,2987 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.animation.Animator;
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
+import android.transition.TransitionSet;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DebugUtils;
+import android.util.SparseArray;
+import android.util.SuperNotCalledException;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * A Fragment is a piece of an application's user interface or behavior
+ * that can be placed in an {@link Activity}. Interaction with fragments
+ * is done through {@link FragmentManager}, which can be obtained via
+ * {@link Activity#getFragmentManager() Activity.getFragmentManager()} and
+ * {@link Fragment#getFragmentManager() Fragment.getFragmentManager()}.
+ *
+ * <p>The Fragment class can be used many ways to achieve a wide variety of
+ * results. In its core, it represents a particular operation or interface
+ * that is running within a larger {@link Activity}. A Fragment is closely
+ * tied to the Activity it is in, and can not be used apart from one. Though
+ * Fragment defines its own lifecycle, that lifecycle is dependent on its
+ * activity: if the activity is stopped, no fragments inside of it can be
+ * started; when the activity is destroyed, all fragments will be destroyed.
+ *
+ * <p>All subclasses of Fragment must include a public no-argument constructor.
+ * The framework will often re-instantiate a fragment class when needed,
+ * in particular during state restore, and needs to be able to find this
+ * constructor to instantiate it. If the no-argument constructor is not
+ * available, a runtime exception will occur in some cases during state
+ * restore.
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#OlderPlatforms">Older Platforms</a>
+ * <li><a href="#Lifecycle">Lifecycle</a>
+ * <li><a href="#Layout">Layout</a>
+ * <li><a href="#BackStack">Back Stack</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using fragments, read the
+ * <a href="{@docRoot}guide/components/fragments.html">Fragments</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="OlderPlatforms"></a>
+ * <h3>Older Platforms</h3>
+ *
+ * While the Fragment API was introduced in
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, a version of the API
+ * at is also available for use on older platforms through
+ * {@link android.support.v4.app.FragmentActivity}. See the blog post
+ * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html">
+ * Fragments For All</a> for more details.
+ *
+ * <a name="Lifecycle"></a>
+ * <h3>Lifecycle</h3>
+ *
+ * <p>Though a Fragment's lifecycle is tied to its owning activity, it has
+ * its own wrinkle on the standard activity lifecycle. It includes basic
+ * activity lifecycle methods such as {@link #onResume}, but also important
+ * are methods related to interactions with the activity and UI generation.
+ *
+ * <p>The core series of lifecycle methods that are called to bring a fragment
+ * up to resumed state (interacting with the user) are:
+ *
+ * <ol>
+ * <li> {@link #onAttach} called once the fragment is associated with its activity.
+ * <li> {@link #onCreate} called to do initial creation of the fragment.
+ * <li> {@link #onCreateView} creates and returns the view hierarchy associated
+ * with the fragment.
+ * <li> {@link #onActivityCreated} tells the fragment that its activity has
+ * completed its own {@link Activity#onCreate Activity.onCreate()}.
+ * <li> {@link #onViewStateRestored} tells the fragment that all of the saved
+ * state of its view hierarchy has been restored.
+ * <li> {@link #onStart} makes the fragment visible to the user (based on its
+ * containing activity being started).
+ * <li> {@link #onResume} makes the fragment begin interacting with the user
+ * (based on its containing activity being resumed).
+ * </ol>
+ *
+ * <p>As a fragment is no longer being used, it goes through a reverse
+ * series of callbacks:
+ *
+ * <ol>
+ * <li> {@link #onPause} fragment is no longer interacting with the user either
+ * because its activity is being paused or a fragment operation is modifying it
+ * in the activity.
+ * <li> {@link #onStop} fragment is no longer visible to the user either
+ * because its activity is being stopped or a fragment operation is modifying it
+ * in the activity.
+ * <li> {@link #onDestroyView} allows the fragment to clean up resources
+ * associated with its View.
+ * <li> {@link #onDestroy} called to do final cleanup of the fragment's state.
+ * <li> {@link #onDetach} called immediately prior to the fragment no longer
+ * being associated with its activity.
+ * </ol>
+ *
+ * <a name="Layout"></a>
+ * <h3>Layout</h3>
+ *
+ * <p>Fragments can be used as part of your application's layout, allowing
+ * you to better modularize your code and more easily adjust your user
+ * interface to the screen it is running on. As an example, we can look
+ * at a simple program consisting of a list of items, and display of the
+ * details of each item.</p>
+ *
+ * <p>An activity's layout XML can include <code>&lt;fragment&gt;</code> tags
+ * to embed fragment instances inside of the layout. For example, here is
+ * a simple layout that embeds one fragment:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/layout/fragment_layout.xml layout}
+ *
+ * <p>The layout is installed in the activity in the normal way:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java
+ * main}
+ *
+ * <p>The titles fragment, showing a list of titles, is fairly simple, relying
+ * on {@link ListFragment} for most of its work. Note the implementation of
+ * clicking an item: depending on the current activity's layout, it can either
+ * create and display a new fragment to show the details in-place (more about
+ * this later), or start a new activity to show the details.</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java
+ * titles}
+ *
+ * <p>The details fragment showing the contents of a selected item just
+ * displays a string of text based on an index of a string array built in to
+ * the app:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java
+ * details}
+ *
+ * <p>In this case when the user clicks on a title, there is no details
+ * container in the current activity, so the titles fragment's click code will
+ * launch a new activity to display the details fragment:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentLayout.java
+ * details_activity}
+ *
+ * <p>However the screen may be large enough to show both the list of titles
+ * and details about the currently selected title. To use such a layout on
+ * a landscape screen, this alternative layout can be placed under layout-land:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/layout-land/fragment_layout.xml layout}
+ *
+ * <p>Note how the prior code will adjust to this alternative UI flow: the titles
+ * fragment will now embed the details fragment inside of this activity, and the
+ * details activity will finish itself if it is running in a configuration
+ * where the details can be shown in-place.
+ *
+ * <p>When a configuration change causes the activity hosting these fragments
+ * to restart, its new instance may use a different layout that doesn't
+ * include the same fragments as the previous layout. In this case all of
+ * the previous fragments will still be instantiated and running in the new
+ * instance. However, any that are no longer associated with a &lt;fragment&gt;
+ * tag in the view hierarchy will not have their content view created
+ * and will return false from {@link #isInLayout}. (The code here also shows
+ * how you can determine if a fragment placed in a container is no longer
+ * running in a layout with that container and avoid creating its view hierarchy
+ * in that case.)
+ *
+ * <p>The attributes of the &lt;fragment&gt; tag are used to control the
+ * LayoutParams provided when attaching the fragment's view to the parent
+ * container. They can also be parsed by the fragment in {@link #onInflate}
+ * as parameters.
+ *
+ * <p>The fragment being instantiated must have some kind of unique identifier
+ * so that it can be re-associated with a previous instance if the parent
+ * activity needs to be destroyed and recreated. This can be provided these
+ * ways:
+ *
+ * <ul>
+ * <li>If nothing is explicitly supplied, the view ID of the container will
+ * be used.
+ * <li><code>android:tag</code> can be used in &lt;fragment&gt; to provide
+ * a specific tag name for the fragment.
+ * <li><code>android:id</code> can be used in &lt;fragment&gt; to provide
+ * a specific identifier for the fragment.
+ * </ul>
+ *
+ * <a name="BackStack"></a>
+ * <h3>Back Stack</h3>
+ *
+ * <p>The transaction in which fragments are modified can be placed on an
+ * internal back-stack of the owning activity. When the user presses back
+ * in the activity, any transactions on the back stack are popped off before
+ * the activity itself is finished.
+ *
+ * <p>For example, consider this simple fragment that is instantiated with
+ * an integer argument and displays that in a TextView in its UI:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentStack.java
+ * fragment}
+ *
+ * <p>A function that creates a new instance of the fragment, replacing
+ * whatever current fragment instance is being shown and pushing that change
+ * on to the back stack could be written as:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentStack.java
+ * add_stack}
+ *
+ * <p>After each call to this function, a new entry is on the stack, and
+ * pressing back will pop it to return the user to whatever previous state
+ * the activity UI was in.
+ */
+public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener {
+ private static final ArrayMap<String, Class<?>> sClassMap =
+ new ArrayMap<String, Class<?>>();
+
+ static final int INVALID_STATE = -1; // Invalid state used as a null value.
+ static final int INITIALIZING = 0; // Not yet created.
+ static final int CREATED = 1; // Created.
+ static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
+ static final int STOPPED = 3; // Fully created, not started.
+ static final int STARTED = 4; // Created and started, not resumed.
+ static final int RESUMED = 5; // Created started and resumed.
+
+ private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet();
+
+ int mState = INITIALIZING;
+
+ // When instantiated from saved state, this is the saved state.
+ Bundle mSavedFragmentState;
+ SparseArray<Parcelable> mSavedViewState;
+
+ // Index into active fragment array.
+ int mIndex = -1;
+
+ // Internal unique name for this fragment;
+ String mWho;
+
+ // Construction arguments;
+ Bundle mArguments;
+
+ // Target fragment.
+ Fragment mTarget;
+
+ // For use when retaining a fragment: this is the index of the last mTarget.
+ int mTargetIndex = -1;
+
+ // Target request code.
+ int mTargetRequestCode;
+
+ // True if the fragment is in the list of added fragments.
+ boolean mAdded;
+
+ // If set this fragment is being removed from its activity.
+ boolean mRemoving;
+
+ // Set to true if this fragment was instantiated from a layout file.
+ boolean mFromLayout;
+
+ // Set to true when the view has actually been inflated in its layout.
+ boolean mInLayout;
+
+ // True if this fragment has been restored from previously saved state.
+ boolean mRestored;
+
+ // True if performCreateView has been called and a matching call to performDestroyView
+ // has not yet happened.
+ boolean mPerformedCreateView;
+
+ // Number of active back stack entries this fragment is in.
+ int mBackStackNesting;
+
+ // The fragment manager we are associated with. Set as soon as the
+ // fragment is used in a transaction; cleared after it has been removed
+ // from all transactions.
+ FragmentManagerImpl mFragmentManager;
+
+ // Activity this fragment is attached to.
+ FragmentHostCallback mHost;
+
+ // Private fragment manager for child fragments inside of this one.
+ FragmentManagerImpl mChildFragmentManager;
+
+ // For use when restoring fragment state and descendant fragments are retained.
+ // This state is set by FragmentState.instantiate and cleared in onCreate.
+ FragmentManagerNonConfig mChildNonConfig;
+
+ // If this Fragment is contained in another Fragment, this is that container.
+ Fragment mParentFragment;
+
+ // The optional identifier for this fragment -- either the container ID if it
+ // was dynamically added to the view hierarchy, or the ID supplied in
+ // layout.
+ int mFragmentId;
+
+ // When a fragment is being dynamically added to the view hierarchy, this
+ // is the identifier of the parent container it is being added to.
+ int mContainerId;
+
+ // The optional named tag for this fragment -- usually used to find
+ // fragments that are not part of the layout.
+ String mTag;
+
+ // Set to true when the app has requested that this fragment be hidden
+ // from the user.
+ boolean mHidden;
+
+ // Set to true when the app has requested that this fragment be detached.
+ boolean mDetached;
+
+ // If set this fragment would like its instance retained across
+ // configuration changes.
+ boolean mRetainInstance;
+
+ // If set this fragment is being retained across the current config change.
+ boolean mRetaining;
+
+ // If set this fragment has menu items to contribute.
+ boolean mHasMenu;
+
+ // Set to true to allow the fragment's menu to be shown.
+ boolean mMenuVisible = true;
+
+ // Used to verify that subclasses call through to super class.
+ boolean mCalled;
+
+ // The parent container of the fragment after dynamically added to UI.
+ ViewGroup mContainer;
+
+ // The View generated for this fragment.
+ View mView;
+
+ // Whether this fragment should defer starting until after other fragments
+ // have been started and their loaders are finished.
+ boolean mDeferStart;
+
+ // Hint provided by the app that this fragment is currently visible to the user.
+ boolean mUserVisibleHint = true;
+
+ LoaderManagerImpl mLoaderManager;
+ boolean mLoadersStarted;
+ boolean mCheckedForLoaderManager;
+
+ // The animation and transition information for the fragment. This will be null
+ // unless the elements are explicitly accessed and should remain null for Fragments
+ // without Views.
+ AnimationInfo mAnimationInfo;
+
+ // True if the View was added, and its animation has yet to be run. This could
+ // also indicate that the fragment view hasn't been made visible, even if there is no
+ // animation for this fragment.
+ boolean mIsNewlyAdded;
+
+ // True if mHidden has been changed and the animation should be scheduled.
+ boolean mHiddenChanged;
+
+ // The cached value from onGetLayoutInflater(Bundle) that will be returned from
+ // getLayoutInflater()
+ LayoutInflater mLayoutInflater;
+
+ // Keep track of whether or not this Fragment has run performCreate(). Retained instance
+ // fragments can have mRetaining set to true without going through creation, so we must
+ // track it separately.
+ boolean mIsCreated;
+
+ /**
+ * State information that has been retrieved from a fragment instance
+ * through {@link FragmentManager#saveFragmentInstanceState(Fragment)
+ * FragmentManager.saveFragmentInstanceState}.
+ */
+ public static class SavedState implements Parcelable {
+ final Bundle mState;
+
+ SavedState(Bundle state) {
+ mState = state;
+ }
+
+ SavedState(Parcel in, ClassLoader loader) {
+ mState = in.readBundle();
+ if (loader != null && mState != null) {
+ mState.setClassLoader(loader);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBundle(mState);
+ }
+
+ public static final Parcelable.ClassLoaderCreator<SavedState> CREATOR
+ = new Parcelable.ClassLoaderCreator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in, null);
+ }
+
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new SavedState(in, loader);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Thrown by {@link Fragment#instantiate(Context, String, Bundle)} when
+ * there is an instantiation failure.
+ */
+ static public class InstantiationException extends AndroidRuntimeException {
+ public InstantiationException(String msg, Exception cause) {
+ super(msg, cause);
+ }
+ }
+
+ /**
+ * Default constructor. <strong>Every</strong> fragment must have an
+ * empty constructor, so it can be instantiated when restoring its
+ * activity's state. It is strongly recommended that subclasses do not
+ * have other constructors with parameters, since these constructors
+ * will not be called when the fragment is re-instantiated; instead,
+ * arguments can be supplied by the caller with {@link #setArguments}
+ * and later retrieved by the Fragment with {@link #getArguments}.
+ *
+ * <p>Applications should generally not implement a constructor. Prefer
+ * {@link #onAttach(Context)} instead. It is the first place application code can run where
+ * the fragment is ready to be used - the point where the fragment is actually associated with
+ * its context. Some applications may also want to implement {@link #onInflate} to retrieve
+ * attributes from a layout resource, although note this happens when the fragment is attached.
+ */
+ public Fragment() {
+ }
+
+ /**
+ * Like {@link #instantiate(Context, String, Bundle)} but with a null
+ * argument Bundle.
+ */
+ public static Fragment instantiate(Context context, String fname) {
+ return instantiate(context, fname, null);
+ }
+
+ /**
+ * Create a new instance of a Fragment with the given class name. This is
+ * the same as calling its empty constructor.
+ *
+ * @param context The calling context being used to instantiate the fragment.
+ * This is currently just used to get its ClassLoader.
+ * @param fname The class name of the fragment to instantiate.
+ * @param args Bundle of arguments to supply to the fragment, which it
+ * can retrieve with {@link #getArguments()}. May be null.
+ * @return Returns a new fragment instance.
+ * @throws InstantiationException If there is a failure in instantiating
+ * the given fragment class. This is a runtime exception; it is not
+ * normally expected to happen.
+ */
+ public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
+ try {
+ Class<?> clazz = sClassMap.get(fname);
+ if (clazz == null) {
+ // Class not found in the cache, see if it's real, and try to add it
+ clazz = context.getClassLoader().loadClass(fname);
+ if (!Fragment.class.isAssignableFrom(clazz)) {
+ throw new InstantiationException("Trying to instantiate a class " + fname
+ + " that is not a Fragment", new ClassCastException());
+ }
+ sClassMap.put(fname, clazz);
+ }
+ Fragment f = (Fragment) clazz.getConstructor().newInstance();
+ if (args != null) {
+ args.setClassLoader(f.getClass().getClassLoader());
+ f.setArguments(args);
+ }
+ return f;
+ } catch (ClassNotFoundException e) {
+ throw new InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (java.lang.InstantiationException e) {
+ throw new InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (IllegalAccessException e) {
+ throw new InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (NoSuchMethodException e) {
+ throw new InstantiationException("Unable to instantiate fragment " + fname
+ + ": could not find Fragment constructor", e);
+ } catch (InvocationTargetException e) {
+ throw new InstantiationException("Unable to instantiate fragment " + fname
+ + ": calling Fragment constructor caused an exception", e);
+ }
+ }
+
+ final void restoreViewState(Bundle savedInstanceState) {
+ if (mSavedViewState != null) {
+ mView.restoreHierarchyState(mSavedViewState);
+ mSavedViewState = null;
+ }
+ mCalled = false;
+ onViewStateRestored(savedInstanceState);
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onViewStateRestored()");
+ }
+ }
+
+ final void setIndex(int index, Fragment parent) {
+ mIndex = index;
+ if (parent != null) {
+ mWho = parent.mWho + ":" + mIndex;
+ } else {
+ mWho = "android:fragment:" + mIndex;
+ }
+ }
+
+ final boolean isInBackStack() {
+ return mBackStackNesting > 0;
+ }
+
+ /**
+ * Subclasses can not override equals().
+ */
+ @Override final public boolean equals(Object o) {
+ return super.equals(o);
+ }
+
+ /**
+ * Subclasses can not override hashCode().
+ */
+ @Override final public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ DebugUtils.buildShortClassTag(this, sb);
+ if (mIndex >= 0) {
+ sb.append(" #");
+ sb.append(mIndex);
+ }
+ if (mFragmentId != 0) {
+ sb.append(" id=0x");
+ sb.append(Integer.toHexString(mFragmentId));
+ }
+ if (mTag != null) {
+ sb.append(" ");
+ sb.append(mTag);
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Return the identifier this fragment is known by. This is either
+ * the android:id value supplied in a layout or the container view ID
+ * supplied when adding the fragment.
+ */
+ final public int getId() {
+ return mFragmentId;
+ }
+
+ /**
+ * Get the tag name of the fragment, if specified.
+ */
+ final public String getTag() {
+ return mTag;
+ }
+
+ /**
+ * Supply the construction arguments for this fragment.
+ * The arguments supplied here will be retained across fragment destroy and
+ * creation.
+ *
+ * <p>This method cannot be called if the fragment is added to a FragmentManager and
+ * if {@link #isStateSaved()} would return true. Prior to {@link Build.VERSION_CODES#O},
+ * this method may only be called if the fragment has not yet been added to a FragmentManager.
+ * </p>
+ */
+ public void setArguments(Bundle args) {
+ // The isStateSaved requirement below was only added in Android O and is compatible
+ // because it loosens previous requirements rather than making them more strict.
+ // See method javadoc.
+ if (mIndex >= 0 && isStateSaved()) {
+ throw new IllegalStateException("Fragment already active");
+ }
+ mArguments = args;
+ }
+
+ /**
+ * Return the arguments supplied to {@link #setArguments}, if any.
+ */
+ final public Bundle getArguments() {
+ return mArguments;
+ }
+
+ /**
+ * Returns true if this fragment is added and its state has already been saved
+ * by its host. Any operations that would change saved state should not be performed
+ * if this method returns true, and some operations such as {@link #setArguments(Bundle)}
+ * will fail.
+ *
+ * @return true if this fragment's state has already been saved by its host
+ */
+ public final boolean isStateSaved() {
+ if (mFragmentManager == null) {
+ return false;
+ }
+ return mFragmentManager.isStateSaved();
+ }
+
+ /**
+ * Set the initial saved state that this Fragment should restore itself
+ * from when first being constructed, as returned by
+ * {@link FragmentManager#saveFragmentInstanceState(Fragment)
+ * FragmentManager.saveFragmentInstanceState}.
+ *
+ * @param state The state the fragment should be restored from.
+ */
+ public void setInitialSavedState(SavedState state) {
+ if (mIndex >= 0) {
+ throw new IllegalStateException("Fragment already active");
+ }
+ mSavedFragmentState = state != null && state.mState != null
+ ? state.mState : null;
+ }
+
+ /**
+ * Optional target for this fragment. This may be used, for example,
+ * if this fragment is being started by another, and when done wants to
+ * give a result back to the first. The target set here is retained
+ * across instances via {@link FragmentManager#putFragment
+ * FragmentManager.putFragment()}.
+ *
+ * @param fragment The fragment that is the target of this one.
+ * @param requestCode Optional request code, for convenience if you
+ * are going to call back with {@link #onActivityResult(int, int, Intent)}.
+ */
+ public void setTargetFragment(Fragment fragment, int requestCode) {
+ // Don't allow a caller to set a target fragment in another FragmentManager,
+ // but there's a snag: people do set target fragments before fragments get added.
+ // We'll have the FragmentManager check that for validity when we move
+ // the fragments to a valid state.
+ final FragmentManager mine = getFragmentManager();
+ final FragmentManager theirs = fragment != null ? fragment.getFragmentManager() : null;
+ if (mine != null && theirs != null && mine != theirs) {
+ throw new IllegalArgumentException("Fragment " + fragment
+ + " must share the same FragmentManager to be set as a target fragment");
+ }
+
+ // Don't let someone create a cycle.
+ for (Fragment check = fragment; check != null; check = check.getTargetFragment()) {
+ if (check == this) {
+ throw new IllegalArgumentException("Setting " + fragment + " as the target of "
+ + this + " would create a target cycle");
+ }
+ }
+ mTarget = fragment;
+ mTargetRequestCode = requestCode;
+ }
+
+ /**
+ * Return the target fragment set by {@link #setTargetFragment}.
+ */
+ final public Fragment getTargetFragment() {
+ return mTarget;
+ }
+
+ /**
+ * Return the target request code set by {@link #setTargetFragment}.
+ */
+ final public int getTargetRequestCode() {
+ return mTargetRequestCode;
+ }
+
+ /**
+ * Return the {@link Context} this fragment is currently associated with.
+ */
+ public Context getContext() {
+ return mHost == null ? null : mHost.getContext();
+ }
+
+ /**
+ * Return the Activity this fragment is currently associated with.
+ */
+ final public Activity getActivity() {
+ return mHost == null ? null : mHost.getActivity();
+ }
+
+ /**
+ * Return the host object of this fragment. May return {@code null} if the fragment
+ * isn't currently being hosted.
+ */
+ @Nullable
+ final public Object getHost() {
+ return mHost == null ? null : mHost.onGetHost();
+ }
+
+ /**
+ * Return <code>getActivity().getResources()</code>.
+ */
+ final public Resources getResources() {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ return mHost.getContext().getResources();
+ }
+
+ /**
+ * Return a localized, styled CharSequence from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the CharSequence text
+ */
+ public final CharSequence getText(@StringRes int resId) {
+ return getResources().getText(resId);
+ }
+
+ /**
+ * Return a localized string from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the string
+ */
+ public final String getString(@StringRes int resId) {
+ return getResources().getString(resId);
+ }
+
+ /**
+ * Return a localized formatted string from the application's package's
+ * default string table, substituting the format arguments as defined in
+ * {@link java.util.Formatter} and {@link java.lang.String#format}.
+ *
+ * @param resId Resource id for the format string
+ * @param formatArgs The format arguments that will be used for substitution.
+ */
+
+ public final String getString(@StringRes int resId, Object... formatArgs) {
+ return getResources().getString(resId, formatArgs);
+ }
+
+ /**
+ * Return the FragmentManager for interacting with fragments associated
+ * with this fragment's activity. Note that this will be non-null slightly
+ * before {@link #getActivity()}, during the time from when the fragment is
+ * placed in a {@link FragmentTransaction} until it is committed and
+ * attached to its activity.
+ *
+ * <p>If this Fragment is a child of another Fragment, the FragmentManager
+ * returned here will be the parent's {@link #getChildFragmentManager()}.
+ */
+ final public FragmentManager getFragmentManager() {
+ return mFragmentManager;
+ }
+
+ /**
+ * Return a private FragmentManager for placing and managing Fragments
+ * inside of this Fragment.
+ */
+ final public FragmentManager getChildFragmentManager() {
+ if (mChildFragmentManager == null) {
+ instantiateChildFragmentManager();
+ if (mState >= RESUMED) {
+ mChildFragmentManager.dispatchResume();
+ } else if (mState >= STARTED) {
+ mChildFragmentManager.dispatchStart();
+ } else if (mState >= ACTIVITY_CREATED) {
+ mChildFragmentManager.dispatchActivityCreated();
+ } else if (mState >= CREATED) {
+ mChildFragmentManager.dispatchCreate();
+ }
+ }
+ return mChildFragmentManager;
+ }
+
+ /**
+ * Returns the parent Fragment containing this Fragment. If this Fragment
+ * is attached directly to an Activity, returns null.
+ */
+ final public Fragment getParentFragment() {
+ return mParentFragment;
+ }
+
+ /**
+ * Return true if the fragment is currently added to its activity.
+ */
+ final public boolean isAdded() {
+ return mHost != null && mAdded;
+ }
+
+ /**
+ * Return true if the fragment has been explicitly detached from the UI.
+ * That is, {@link FragmentTransaction#detach(Fragment)
+ * FragmentTransaction.detach(Fragment)} has been used on it.
+ */
+ final public boolean isDetached() {
+ return mDetached;
+ }
+
+ /**
+ * Return true if this fragment is currently being removed from its
+ * activity. This is <em>not</em> whether its activity is finishing, but
+ * rather whether it is in the process of being removed from its activity.
+ */
+ final public boolean isRemoving() {
+ return mRemoving;
+ }
+
+ /**
+ * Return true if the layout is included as part of an activity view
+ * hierarchy via the &lt;fragment&gt; tag. This will always be true when
+ * fragments are created through the &lt;fragment&gt; tag, <em>except</em>
+ * in the case where an old fragment is restored from a previous state and
+ * it does not appear in the layout of the current state.
+ */
+ final public boolean isInLayout() {
+ return mInLayout;
+ }
+
+ /**
+ * Return true if the fragment is in the resumed state. This is true
+ * for the duration of {@link #onResume()} and {@link #onPause()} as well.
+ */
+ final public boolean isResumed() {
+ return mState >= RESUMED;
+ }
+
+ /**
+ * Return true if the fragment is currently visible to the user. This means
+ * it: (1) has been added, (2) has its view attached to the window, and
+ * (3) is not hidden.
+ */
+ final public boolean isVisible() {
+ return isAdded() && !isHidden() && mView != null
+ && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Return true if the fragment has been hidden. By default fragments
+ * are shown. You can find out about changes to this state with
+ * {@link #onHiddenChanged}. Note that the hidden state is orthogonal
+ * to other states -- that is, to be visible to the user, a fragment
+ * must be both started and not hidden.
+ */
+ final public boolean isHidden() {
+ return mHidden;
+ }
+
+ /**
+ * Called when the hidden state (as returned by {@link #isHidden()} of
+ * the fragment has changed. Fragments start out not hidden; this will
+ * be called whenever the fragment changes state from that.
+ * @param hidden True if the fragment is now hidden, false otherwise.
+ */
+ public void onHiddenChanged(boolean hidden) {
+ }
+
+ /**
+ * Control whether a fragment instance is retained across Activity
+ * re-creation (such as from a configuration change). This can only
+ * be used with fragments not in the back stack. If set, the fragment
+ * lifecycle will be slightly different when an activity is recreated:
+ * <ul>
+ * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still
+ * will be, because the fragment is being detached from its current activity).
+ * <li> {@link #onCreate(Bundle)} will not be called since the fragment
+ * is not being re-created.
+ * <li> {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} <b>will</b>
+ * still be called.
+ * </ul>
+ */
+ public void setRetainInstance(boolean retain) {
+ mRetainInstance = retain;
+ }
+
+ final public boolean getRetainInstance() {
+ return mRetainInstance;
+ }
+
+ /**
+ * Report that this fragment would like to participate in populating
+ * the options menu by receiving a call to {@link #onCreateOptionsMenu}
+ * and related methods.
+ *
+ * @param hasMenu If true, the fragment has menu items to contribute.
+ */
+ public void setHasOptionsMenu(boolean hasMenu) {
+ if (mHasMenu != hasMenu) {
+ mHasMenu = hasMenu;
+ if (isAdded() && !isHidden()) {
+ mFragmentManager.invalidateOptionsMenu();
+ }
+ }
+ }
+
+ /**
+ * Set a hint for whether this fragment's menu should be visible. This
+ * is useful if you know that a fragment has been placed in your view
+ * hierarchy so that the user can not currently seen it, so any menu items
+ * it has should also not be shown.
+ *
+ * @param menuVisible The default is true, meaning the fragment's menu will
+ * be shown as usual. If false, the user will not see the menu.
+ */
+ public void setMenuVisibility(boolean menuVisible) {
+ if (mMenuVisible != menuVisible) {
+ mMenuVisible = menuVisible;
+ if (mHasMenu && isAdded() && !isHidden()) {
+ mFragmentManager.invalidateOptionsMenu();
+ }
+ }
+ }
+
+ /**
+ * Set a hint to the system about whether this fragment's UI is currently visible
+ * to the user. This hint defaults to true and is persistent across fragment instance
+ * state save and restore.
+ *
+ * <p>An app may set this to false to indicate that the fragment's UI is
+ * scrolled out of visibility or is otherwise not directly visible to the user.
+ * This may be used by the system to prioritize operations such as fragment lifecycle updates
+ * or loader ordering behavior.</p>
+ *
+ * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle
+ * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
+ *
+ * <p><strong>Note:</strong> Prior to Android N there was a platform bug that could cause
+ * <code>setUserVisibleHint</code> to bring a fragment up to the started state before its
+ * <code>FragmentTransaction</code> had been committed. As some apps relied on this behavior,
+ * it is preserved for apps that declare a <code>targetSdkVersion</code> of 23 or lower.</p>
+ *
+ * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
+ * false if it is not.
+ */
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ // Prior to Android N we were simply checking if this fragment had a FragmentManager
+ // set before we would trigger a deferred start. Unfortunately this also gets set before
+ // a fragment transaction is committed, so if setUserVisibleHint was called before a
+ // transaction commit, we would start the fragment way too early. FragmentPagerAdapter
+ // triggers this situation.
+ // Unfortunately some apps relied on this timing in overrides of setUserVisibleHint
+ // on their own fragments, and expected, however erroneously, that after a call to
+ // super.setUserVisibleHint their onStart methods had been run.
+ // We preserve this behavior for apps targeting old platform versions below.
+ boolean useBrokenAddedCheck = false;
+ Context context = getContext();
+ if (mFragmentManager != null && mFragmentManager.mHost != null) {
+ context = mFragmentManager.mHost.getContext();
+ }
+ if (context != null) {
+ useBrokenAddedCheck = context.getApplicationInfo().targetSdkVersion <= VERSION_CODES.M;
+ }
+
+ final boolean performDeferredStart;
+ if (useBrokenAddedCheck) {
+ performDeferredStart = !mUserVisibleHint && isVisibleToUser && mState < STARTED
+ && mFragmentManager != null;
+ } else {
+ performDeferredStart = !mUserVisibleHint && isVisibleToUser && mState < STARTED
+ && mFragmentManager != null && isAdded();
+ }
+
+ if (performDeferredStart) {
+ mFragmentManager.performPendingDeferredStart(this);
+ }
+
+ mUserVisibleHint = isVisibleToUser;
+ mDeferStart = mState < STARTED && !isVisibleToUser;
+ }
+
+ /**
+ * @return The current value of the user-visible hint on this fragment.
+ * @see #setUserVisibleHint(boolean)
+ */
+ public boolean getUserVisibleHint() {
+ return mUserVisibleHint;
+ }
+
+ /**
+ * Return the LoaderManager for this fragment, creating it if needed.
+ */
+ public LoaderManager getLoaderManager() {
+ if (mLoaderManager != null) {
+ return mLoaderManager;
+ }
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ mCheckedForLoaderManager = true;
+ mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, true);
+ return mLoaderManager;
+ }
+
+ /**
+ * Call {@link Activity#startActivity(Intent)} from the fragment's
+ * containing Activity.
+ *
+ * @param intent The intent to start.
+ */
+ public void startActivity(Intent intent) {
+ startActivity(intent, null);
+ }
+
+ /**
+ * Call {@link Activity#startActivity(Intent, Bundle)} from the fragment's
+ * containing Activity.
+ *
+ * @param intent The intent to start.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ */
+ public void startActivity(Intent intent, Bundle options) {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ if (options != null) {
+ mHost.onStartActivityFromFragment(this, intent, -1, options);
+ } else {
+ // Note we want to go through this call for compatibility with
+ // applications that may have overridden the method.
+ mHost.onStartActivityFromFragment(this, intent, -1, null /*options*/);
+ }
+ }
+
+ /**
+ * Call {@link Activity#startActivityForResult(Intent, int)} from the fragment's
+ * containing Activity.
+ */
+ public void startActivityForResult(Intent intent, int requestCode) {
+ startActivityForResult(intent, requestCode, null);
+ }
+
+ /**
+ * Call {@link Activity#startActivityForResult(Intent, int, Bundle)} from the fragment's
+ * containing Activity.
+ */
+ public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ mHost.onStartActivityFromFragment(this, intent, requestCode, options);
+ }
+
+ /**
+ * @hide
+ * Call {@link Activity#startActivityForResultAsUser(Intent, int, UserHandle)} from the
+ * fragment's containing Activity.
+ */
+ public void startActivityForResultAsUser(
+ Intent intent, int requestCode, Bundle options, UserHandle user) {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ mHost.onStartActivityAsUserFromFragment(this, intent, requestCode, options, user);
+ }
+
+ /**
+ * Call {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int,
+ * Bundle)} from the fragment's containing Activity.
+ */
+ public void startIntentSenderForResult(IntentSender intent, int requestCode,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
+ Bundle options) throws IntentSender.SendIntentException {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ mHost.onStartIntentSenderFromFragment(this, intent, requestCode, fillInIntent, flagsMask,
+ flagsValues, extraFlags, options);
+ }
+
+ /**
+ * Receive the result from a previous call to
+ * {@link #startActivityForResult(Intent, int)}. This follows the
+ * related Activity API as described there in
+ * {@link Activity#onActivityResult(int, int, Intent)}.
+ *
+ * @param requestCode The integer request code originally supplied to
+ * startActivityForResult(), allowing you to identify who this
+ * result came from.
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ /**
+ * Requests permissions to be granted to this application. These permissions
+ * must be requested in your manifest, they should not be granted to your app,
+ * and they should have protection level {@link android.content.pm.PermissionInfo
+ * #PROTECTION_DANGEROUS dangerous}, regardless whether they are declared by
+ * the platform or a third-party app.
+ * <p>
+ * Normal permissions {@link android.content.pm.PermissionInfo#PROTECTION_NORMAL}
+ * are granted at install time if requested in the manifest. Signature permissions
+ * {@link android.content.pm.PermissionInfo#PROTECTION_SIGNATURE} are granted at
+ * install time if requested in the manifest and the signature of your app matches
+ * the signature of the app declaring the permissions.
+ * </p>
+ * <p>
+ * If your app does not have the requested permissions the user will be presented
+ * with UI for accepting them. After the user has accepted or rejected the
+ * requested permissions you will receive a callback on {@link
+ * #onRequestPermissionsResult(int, String[], int[])} reporting whether the
+ * permissions were granted or not.
+ * </p>
+ * <p>
+ * Note that requesting a permission does not guarantee it will be granted and
+ * your app should be able to run without having this permission.
+ * </p>
+ * <p>
+ * This method may start an activity allowing the user to choose which permissions
+ * to grant and which to reject. Hence, you should be prepared that your activity
+ * may be paused and resumed. Further, granting some permissions may require
+ * a restart of you application. In such a case, the system will recreate the
+ * activity stack before delivering the result to {@link
+ * #onRequestPermissionsResult(int, String[], int[])}.
+ * </p>
+ * <p>
+ * When checking whether you have a permission you should use {@link
+ * android.content.Context#checkSelfPermission(String)}.
+ * </p>
+ * <p>
+ * Calling this API for permissions already granted to your app would show UI
+ * to the user to decide whether the app can still hold these permissions. This
+ * can be useful if the way your app uses data guarded by the permissions
+ * changes significantly.
+ * </p>
+ * <p>
+ * You cannot request a permission if your activity sets {@link
+ * android.R.styleable#AndroidManifestActivity_noHistory noHistory} to
+ * <code>true</code> because in this case the activity would not receive
+ * result callbacks including {@link #onRequestPermissionsResult(int, String[], int[])}.
+ * </p>
+ * <p>
+ * A sample permissions request looks like this:
+ * </p>
+ * <code><pre><p>
+ * private void showContacts() {
+ * if (getActivity().checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ * != PackageManager.PERMISSION_GRANTED) {
+ * requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
+ * PERMISSIONS_REQUEST_READ_CONTACTS);
+ * } else {
+ * doShowContacts();
+ * }
+ * }
+ *
+ * {@literal @}Override
+ * public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ * int[] grantResults) {
+ * if (requestCode == PERMISSIONS_REQUEST_READ_CONTACTS
+ * && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ * doShowContacts();
+ * }
+ * }
+ * </code></pre></p>
+ *
+ * @param permissions The requested permissions. Must me non-null and not empty.
+ * @param requestCode Application specific request code to match with a result
+ * reported to {@link #onRequestPermissionsResult(int, String[], int[])}.
+ * Should be >= 0.
+ *
+ * @see #onRequestPermissionsResult(int, String[], int[])
+ * @see android.content.Context#checkSelfPermission(String)
+ */
+ public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
+ if (mHost == null) {
+ throw new IllegalStateException("Fragment " + this + " not attached to Activity");
+ }
+ mHost.onRequestPermissionsFromFragment(this, permissions,requestCode);
+ }
+
+ /**
+ * Callback for the result from requesting permissions. This method
+ * is invoked for every call on {@link #requestPermissions(String[], int)}.
+ * <p>
+ * <strong>Note:</strong> It is possible that the permissions request interaction
+ * with the user is interrupted. In this case you will receive empty permissions
+ * and results arrays which should be treated as a cancellation.
+ * </p>
+ *
+ * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}.
+ * @param permissions The requested permissions. Never null.
+ * @param grantResults The grant results for the corresponding permissions
+ * which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}
+ * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.
+ *
+ * @see #requestPermissions(String[], int)
+ */
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ /* callback - do nothing */
+ }
+
+ /**
+ * Gets whether you should show UI with rationale for requesting a permission.
+ * You should do this only if you do not have the permission and the context in
+ * which the permission is requested does not clearly communicate to the user
+ * what would be the benefit from granting this permission.
+ * <p>
+ * For example, if you write a camera app, requesting the camera permission
+ * would be expected by the user and no rationale for why it is requested is
+ * needed. If however, the app needs location for tagging photos then a non-tech
+ * savvy user may wonder how location is related to taking photos. In this case
+ * you may choose to show UI with rationale of requesting this permission.
+ * </p>
+ *
+ * @param permission A permission your app wants to request.
+ * @return Whether you can show permission rationale UI.
+ *
+ * @see Context#checkSelfPermission(String)
+ * @see #requestPermissions(String[], int)
+ * @see #onRequestPermissionsResult(int, String[], int[])
+ */
+ public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
+ if (mHost != null) {
+ return mHost.getContext().getPackageManager()
+ .shouldShowRequestPermissionRationale(permission);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the LayoutInflater used to inflate Views of this Fragment. The default
+ * implementation will throw an exception if the Fragment is not attached.
+ *
+ * @return The LayoutInflater used to inflate Views of this Fragment.
+ */
+ public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
+ if (mHost == null) {
+ throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "
+ + "Fragment is attached to the FragmentManager.");
+ }
+ final LayoutInflater result = mHost.onGetLayoutInflater();
+ if (mHost.onUseFragmentManagerInflaterFactory()) {
+ getChildFragmentManager(); // Init if needed; use raw implementation below.
+ result.setPrivateFactory(mChildFragmentManager.getLayoutInflaterFactory());
+ }
+ return result;
+ }
+
+ /**
+ * Returns the cached LayoutInflater used to inflate Views of this Fragment. If
+ * {@link #onGetLayoutInflater(Bundle)} has not been called {@link #onGetLayoutInflater(Bundle)}
+ * will be called with a {@code null} argument and that value will be cached.
+ * <p>
+ * The cached LayoutInflater will be replaced immediately prior to
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} and cleared immediately after
+ * {@link #onDetach()}.
+ *
+ * @return The LayoutInflater used to inflate Views of this Fragment.
+ */
+ public final LayoutInflater getLayoutInflater() {
+ if (mLayoutInflater == null) {
+ return performGetLayoutInflater(null);
+ }
+ return mLayoutInflater;
+ }
+
+ /**
+ * Calls {@link #onGetLayoutInflater(Bundle)} and caches the result for use by
+ * {@link #getLayoutInflater()}.
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ * @return The LayoutInflater used to inflate Views of this Fragment.
+ */
+ LayoutInflater performGetLayoutInflater(Bundle savedInstanceState) {
+ LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState);
+ mLayoutInflater = layoutInflater;
+ return mLayoutInflater;
+ }
+
+ /**
+ * @deprecated Use {@link #onInflate(Context, AttributeSet, Bundle)} instead.
+ */
+ @Deprecated
+ @CallSuper
+ public void onInflate(AttributeSet attrs, Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when a fragment is being created as part of a view layout
+ * inflation, typically from setting the content view of an activity. This
+ * may be called immediately after the fragment is created from a <fragment>
+ * tag in a layout file. Note this is <em>before</em> the fragment's
+ * {@link #onAttach(Activity)} has been called; all you should do here is
+ * parse the attributes and save them away.
+ *
+ * <p>This is called every time the fragment is inflated, even if it is
+ * being inflated into a new instance with saved state. It typically makes
+ * sense to re-parse the parameters each time, to allow them to change with
+ * different configurations.</p>
+ *
+ * <p>Here is a typical implementation of a fragment that can take parameters
+ * both through attributes supplied here as well from {@link #getArguments()}:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java
+ * fragment}
+ *
+ * <p>Note that parsing the XML attributes uses a "styleable" resource. The
+ * declaration for the styleable used here is:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/values/attrs.xml fragment_arguments}
+ *
+ * <p>The fragment can then be declared within its activity's content layout
+ * through a tag like this:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/layout/fragment_arguments.xml from_attributes}
+ *
+ * <p>This fragment can also be created dynamically from arguments given
+ * at runtime in the arguments Bundle; here is an example of doing so at
+ * creation of the containing activity:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java
+ * create}
+ *
+ * @param context The Context that is inflating this fragment.
+ * @param attrs The attributes at the tag where the fragment is
+ * being created.
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ @CallSuper
+ public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
+ onInflate(attrs, savedInstanceState);
+ mCalled = true;
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Fragment);
+ setEnterTransition(loadTransition(context, a, getEnterTransition(), null,
+ com.android.internal.R.styleable.Fragment_fragmentEnterTransition));
+ setReturnTransition(loadTransition(context, a, getReturnTransition(),
+ USE_DEFAULT_TRANSITION,
+ com.android.internal.R.styleable.Fragment_fragmentReturnTransition));
+ setExitTransition(loadTransition(context, a, getExitTransition(), null,
+ com.android.internal.R.styleable.Fragment_fragmentExitTransition));
+
+ setReenterTransition(loadTransition(context, a, getReenterTransition(),
+ USE_DEFAULT_TRANSITION,
+ com.android.internal.R.styleable.Fragment_fragmentReenterTransition));
+ setSharedElementEnterTransition(loadTransition(context, a,
+ getSharedElementEnterTransition(), null,
+ com.android.internal.R.styleable.Fragment_fragmentSharedElementEnterTransition));
+ setSharedElementReturnTransition(loadTransition(context, a,
+ getSharedElementReturnTransition(), USE_DEFAULT_TRANSITION,
+ com.android.internal.R.styleable.Fragment_fragmentSharedElementReturnTransition));
+ boolean isEnterSet;
+ boolean isReturnSet;
+ if (mAnimationInfo == null) {
+ isEnterSet = false;
+ isReturnSet = false;
+ } else {
+ isEnterSet = mAnimationInfo.mAllowEnterTransitionOverlap != null;
+ isReturnSet = mAnimationInfo.mAllowReturnTransitionOverlap != null;
+ }
+ if (!isEnterSet) {
+ setAllowEnterTransitionOverlap(a.getBoolean(
+ com.android.internal.R.styleable.Fragment_fragmentAllowEnterTransitionOverlap,
+ true));
+ }
+ if (!isReturnSet) {
+ setAllowReturnTransitionOverlap(a.getBoolean(
+ com.android.internal.R.styleable.Fragment_fragmentAllowReturnTransitionOverlap,
+ true));
+ }
+ a.recycle();
+
+ final Activity hostActivity = mHost == null ? null : mHost.getActivity();
+ if (hostActivity != null) {
+ mCalled = false;
+ onInflate(hostActivity, attrs, savedInstanceState);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #onInflate(Context, AttributeSet, Bundle)} instead.
+ */
+ @Deprecated
+ @CallSuper
+ public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when a fragment is attached as a child of this fragment.
+ *
+ * <p>This is called after the attached fragment's <code>onAttach</code> and before
+ * the attached fragment's <code>onCreate</code> if the fragment has not yet had a previous
+ * call to <code>onCreate</code>.</p>
+ *
+ * @param childFragment child fragment being attached
+ */
+ public void onAttachFragment(Fragment childFragment) {
+ }
+
+ /**
+ * Called when a fragment is first attached to its context.
+ * {@link #onCreate(Bundle)} will be called after this.
+ */
+ @CallSuper
+ public void onAttach(Context context) {
+ mCalled = true;
+ final Activity hostActivity = mHost == null ? null : mHost.getActivity();
+ if (hostActivity != null) {
+ mCalled = false;
+ onAttach(hostActivity);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #onAttach(Context)} instead.
+ */
+ @Deprecated
+ @CallSuper
+ public void onAttach(Activity activity) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when a fragment loads an animation.
+ */
+ public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
+ return null;
+ }
+
+ /**
+ * Called to do initial creation of a fragment. This is called after
+ * {@link #onAttach(Activity)} and before
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, but is not called if the fragment
+ * instance is retained across Activity re-creation (see {@link #setRetainInstance(boolean)}).
+ *
+ * <p>Note that this can be called while the fragment's activity is
+ * still in the process of being created. As such, you can not rely
+ * on things like the activity's content view hierarchy being initialized
+ * at this point. If you want to do work once the activity itself is
+ * created, see {@link #onActivityCreated(Bundle)}.
+ *
+ * <p>If your app's <code>targetSdkVersion</code> is {@link android.os.Build.VERSION_CODES#M}
+ * or lower, child fragments being restored from the savedInstanceState are restored after
+ * <code>onCreate</code> returns. When targeting {@link android.os.Build.VERSION_CODES#N} or
+ * above and running on an N or newer platform version
+ * they are restored by <code>Fragment.onCreate</code>.</p>
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ @CallSuper
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ mCalled = true;
+ final Context context = getContext();
+ final int version = context != null ? context.getApplicationInfo().targetSdkVersion : 0;
+ if (version >= Build.VERSION_CODES.N) {
+ restoreChildFragmentState(savedInstanceState, true);
+ if (mChildFragmentManager != null
+ && !mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {
+ mChildFragmentManager.dispatchCreate();
+ }
+ }
+ }
+
+ void restoreChildFragmentState(@Nullable Bundle savedInstanceState, boolean provideNonConfig) {
+ if (savedInstanceState != null) {
+ Parcelable p = savedInstanceState.getParcelable(Activity.FRAGMENTS_TAG);
+ if (p != null) {
+ if (mChildFragmentManager == null) {
+ instantiateChildFragmentManager();
+ }
+ mChildFragmentManager.restoreAllState(p, provideNonConfig ? mChildNonConfig : null);
+ mChildNonConfig = null;
+ mChildFragmentManager.dispatchCreate();
+ }
+ }
+ }
+
+ /**
+ * Called to have the fragment instantiate its user interface view.
+ * This is optional, and non-graphical fragments can return null (which
+ * is the default implementation). This will be called between
+ * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
+ *
+ * <p>If you return a View from here, you will later be called in
+ * {@link #onDestroyView} when the view is being released.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate
+ * any views in the fragment,
+ * @param container If non-null, this is the parent view that the fragment's
+ * UI should be attached to. The fragment should not add the view itself,
+ * but this can be used to generate the LayoutParams of the view.
+ * @param savedInstanceState If non-null, this fragment is being re-constructed
+ * from a previous saved state as given here.
+ *
+ * @return Return the View for the fragment's UI, or null.
+ */
+ @Nullable
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
+ return null;
+ }
+
+ /**
+ * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}
+ * has returned, but before any saved state has been restored in to the view.
+ * This gives subclasses a chance to initialize themselves once
+ * they know their view hierarchy has been completely created. The fragment's
+ * view hierarchy is not however attached to its parent at this point.
+ * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ * @param savedInstanceState If non-null, this fragment is being re-constructed
+ * from a previous saved state as given here.
+ */
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ }
+
+ /**
+ * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}),
+ * if provided.
+ *
+ * @return The fragment's root view, or null if it has no layout.
+ */
+ @Nullable
+ public View getView() {
+ return mView;
+ }
+
+ /**
+ * Called when the fragment's activity has been created and this
+ * fragment's view hierarchy instantiated. It can be used to do final
+ * initialization once these pieces are in place, such as retrieving
+ * views or restoring state. It is also useful for fragments that use
+ * {@link #setRetainInstance(boolean)} to retain their instance,
+ * as this callback tells the fragment when it is fully associated with
+ * the new activity instance. This is called after {@link #onCreateView}
+ * and before {@link #onViewStateRestored(Bundle)}.
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ @CallSuper
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when all saved state has been restored into the view hierarchy
+ * of the fragment. This can be used to do initialization based on saved
+ * state that you are letting the view hierarchy track itself, such as
+ * whether check box widgets are currently checked. This is called
+ * after {@link #onActivityCreated(Bundle)} and before
+ * {@link #onStart()}.
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ @CallSuper
+ public void onViewStateRestored(Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is visible to the user. This is generally
+ * tied to {@link Activity#onStart() Activity.onStart} of the containing
+ * Activity's lifecycle.
+ */
+ @CallSuper
+ public void onStart() {
+ mCalled = true;
+
+ if (!mLoadersStarted) {
+ mLoadersStarted = true;
+ if (!mCheckedForLoaderManager) {
+ mCheckedForLoaderManager = true;
+ mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, false);
+ } else if (mLoaderManager != null) {
+ mLoaderManager.doStart();
+ }
+ }
+ }
+
+ /**
+ * Called when the fragment is visible to the user and actively running.
+ * This is generally
+ * tied to {@link Activity#onResume() Activity.onResume} of the containing
+ * Activity's lifecycle.
+ */
+ @CallSuper
+ public void onResume() {
+ mCalled = true;
+ }
+
+ /**
+ * Called to ask the fragment to save its current dynamic state, so it
+ * can later be reconstructed in a new instance of its process is
+ * restarted. If a new instance of the fragment later needs to be
+ * created, the data you place in the Bundle here will be available
+ * in the Bundle given to {@link #onCreate(Bundle)},
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and
+ * {@link #onActivityCreated(Bundle)}.
+ *
+ * <p>This corresponds to {@link Activity#onSaveInstanceState(Bundle)
+ * Activity.onSaveInstanceState(Bundle)} and most of the discussion there
+ * applies here as well. Note however: <em>this method may be called
+ * at any time before {@link #onDestroy()}</em>. There are many situations
+ * where a fragment may be mostly torn down (such as when placed on the
+ * back stack with no UI showing), but its state will not be saved until
+ * its owning activity actually needs to save its state.
+ *
+ * @param outState Bundle in which to place your saved state.
+ */
+ public void onSaveInstanceState(Bundle outState) {
+ }
+
+ /**
+ * Called when the Fragment's activity changes from fullscreen mode to multi-window mode and
+ * visa-versa. This is generally tied to {@link Activity#onMultiWindowModeChanged} of the
+ * containing Activity. This method provides the same configuration that will be sent in the
+ * following {@link #onConfigurationChanged(Configuration)} call after the activity enters this
+ * mode.
+ *
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
+ * @param newConfig The new configuration of the activity with the state
+ * {@param isInMultiWindowMode}.
+ */
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
+ onMultiWindowModeChanged(isInMultiWindowMode);
+ }
+
+ /**
+ * Called when the Fragment's activity changes from fullscreen mode to multi-window mode and
+ * visa-versa. This is generally tied to {@link Activity#onMultiWindowModeChanged} of the
+ * containing Activity.
+ *
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
+ *
+ * @deprecated Use {@link #onMultiWindowModeChanged(boolean, Configuration)} instead.
+ */
+ @Deprecated
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ }
+
+ /**
+ * Called by the system when the activity changes to and from picture-in-picture mode. This is
+ * generally tied to {@link Activity#onPictureInPictureModeChanged} of the containing Activity.
+ * This method provides the same configuration that will be sent in the following
+ * {@link #onConfigurationChanged(Configuration)} call after the activity enters this mode.
+ *
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
+ * @param newConfig The new configuration of the activity with the state
+ * {@param isInPictureInPictureMode}.
+ */
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ onPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+
+ /**
+ * Called by the system when the activity changes to and from picture-in-picture mode. This is
+ * generally tied to {@link Activity#onPictureInPictureModeChanged} of the containing Activity.
+ *
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
+ *
+ * @deprecated Use {@link #onPictureInPictureModeChanged(boolean, Configuration)} instead.
+ */
+ @Deprecated
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ }
+
+ @CallSuper
+ public void onConfigurationChanged(Configuration newConfig) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is no longer resumed. This is generally
+ * tied to {@link Activity#onPause() Activity.onPause} of the containing
+ * Activity's lifecycle.
+ */
+ @CallSuper
+ public void onPause() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is no longer started. This is generally
+ * tied to {@link Activity#onStop() Activity.onStop} of the containing
+ * Activity's lifecycle.
+ */
+ @CallSuper
+ public void onStop() {
+ mCalled = true;
+ }
+
+ @CallSuper
+ public void onLowMemory() {
+ mCalled = true;
+ }
+
+ @CallSuper
+ public void onTrimMemory(int level) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the view previously created by {@link #onCreateView} has
+ * been detached from the fragment. The next time the fragment needs
+ * to be displayed, a new view will be created. This is called
+ * after {@link #onStop()} and before {@link #onDestroy()}. It is called
+ * <em>regardless</em> of whether {@link #onCreateView} returned a
+ * non-null view. Internally it is called after the view's state has
+ * been saved but before it has been removed from its parent.
+ */
+ @CallSuper
+ public void onDestroyView() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the fragment is no longer in use. This is called
+ * after {@link #onStop()} and before {@link #onDetach()}.
+ */
+ @CallSuper
+ public void onDestroy() {
+ mCalled = true;
+ //Log.v("foo", "onDestroy: mCheckedForLoaderManager=" + mCheckedForLoaderManager
+ // + " mLoaderManager=" + mLoaderManager);
+ if (!mCheckedForLoaderManager) {
+ mCheckedForLoaderManager = true;
+ mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, false);
+ }
+ if (mLoaderManager != null) {
+ mLoaderManager.doDestroy();
+ }
+ }
+
+ /**
+ * Called by the fragment manager once this fragment has been removed,
+ * so that we don't have any left-over state if the application decides
+ * to re-use the instance. This only clears state that the framework
+ * internally manages, not things the application sets.
+ */
+ void initState() {
+ mIndex = -1;
+ mWho = null;
+ mAdded = false;
+ mRemoving = false;
+ mFromLayout = false;
+ mInLayout = false;
+ mRestored = false;
+ mBackStackNesting = 0;
+ mFragmentManager = null;
+ mChildFragmentManager = null;
+ mHost = null;
+ mFragmentId = 0;
+ mContainerId = 0;
+ mTag = null;
+ mHidden = false;
+ mDetached = false;
+ mRetaining = false;
+ mLoaderManager = null;
+ mLoadersStarted = false;
+ mCheckedForLoaderManager = false;
+ }
+
+ /**
+ * Called when the fragment is no longer attached to its activity. This is called after
+ * {@link #onDestroy()}, except in the cases where the fragment instance is retained across
+ * Activity re-creation (see {@link #setRetainInstance(boolean)}), in which case it is called
+ * after {@link #onStop()}.
+ */
+ @CallSuper
+ public void onDetach() {
+ mCalled = true;
+ }
+
+ /**
+ * Initialize the contents of the Activity's standard options menu. You
+ * should place your menu items in to <var>menu</var>. For this method
+ * to be called, you must have first called {@link #setHasOptionsMenu}. See
+ * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu}
+ * for more information.
+ *
+ * @param menu The options menu in which you place your items.
+ *
+ * @see #setHasOptionsMenu
+ * @see #onPrepareOptionsMenu
+ * @see #onOptionsItemSelected
+ */
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ }
+
+ /**
+ * Prepare the Screen's standard options menu to be displayed. This is
+ * called right before the menu is shown, every time it is shown. You can
+ * use this method to efficiently enable/disable items or otherwise
+ * dynamically modify the contents. See
+ * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu}
+ * for more information.
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ *
+ * @see #setHasOptionsMenu
+ * @see #onCreateOptionsMenu
+ */
+ public void onPrepareOptionsMenu(Menu menu) {
+ }
+
+ /**
+ * Called when this fragment's option menu items are no longer being
+ * included in the overall options menu. Receiving this call means that
+ * the menu needed to be rebuilt, but this fragment's items were not
+ * included in the newly built menu (its {@link #onCreateOptionsMenu(Menu, MenuInflater)}
+ * was not called).
+ */
+ public void onDestroyOptionsMenu() {
+ }
+
+ /**
+ * This hook is called whenever an item in your options menu is selected.
+ * The default implementation simply returns false to have the normal
+ * processing happen (calling the item's Runnable or sending a message to
+ * its Handler as appropriate). You can use this method for any items
+ * for which you would like to do processing without those other
+ * facilities.
+ *
+ * <p>Derived classes should call through to the base class for it to
+ * perform the default menu handling.
+ *
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return false to allow normal menu processing to
+ * proceed, true to consume it here.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return false;
+ }
+
+ /**
+ * This hook is called whenever the options menu is being closed (either by the user canceling
+ * the menu with the back/menu button, or when an item is selected).
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ */
+ public void onOptionsMenuClosed(Menu menu) {
+ }
+
+ /**
+ * Called when a context menu for the {@code view} is about to be shown.
+ * Unlike {@link #onCreateOptionsMenu}, this will be called every
+ * time the context menu is about to be shown and should be populated for
+ * the view (or item inside the view for {@link AdapterView} subclasses,
+ * this can be found in the {@code menuInfo})).
+ * <p>
+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an
+ * item has been selected.
+ * <p>
+ * The default implementation calls up to
+ * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though
+ * you can not call this implementation if you don't want that behavior.
+ * <p>
+ * It is not safe to hold onto the context menu after this method returns.
+ * {@inheritDoc}
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ getActivity().onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ /**
+ * Registers a context menu to be shown for the given view (multiple views
+ * can show the context menu). This method will set the
+ * {@link OnCreateContextMenuListener} on the view to this fragment, so
+ * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be
+ * called when it is time to show the context menu.
+ *
+ * @see #unregisterForContextMenu(View)
+ * @param view The view that should show a context menu.
+ */
+ public void registerForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * Prevents a context menu to be shown for the given view. This method will
+ * remove the {@link OnCreateContextMenuListener} on the view.
+ *
+ * @see #registerForContextMenu(View)
+ * @param view The view that should stop showing a context menu.
+ */
+ public void unregisterForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * This hook is called whenever an item in a context menu is selected. The
+ * default implementation simply returns false to have the normal processing
+ * happen (calling the item's Runnable or sending a message to its Handler
+ * as appropriate). You can use this method for any items for which you
+ * would like to do processing without those other facilities.
+ * <p>
+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the
+ * View that added this menu item.
+ * <p>
+ * Derived classes should call through to the base class for it to perform
+ * the default menu handling.
+ *
+ * @param item The context menu item that was selected.
+ * @return boolean Return false to allow normal context menu processing to
+ * proceed, true to consume it here.
+ */
+ public boolean onContextItemSelected(MenuItem item) {
+ return false;
+ }
+
+ /**
+ * When custom transitions are used with Fragments, the enter transition callback
+ * is called when this Fragment is attached or detached when not popping the back stack.
+ *
+ * @param callback Used to manipulate the shared element transitions on this Fragment
+ * when added not as a pop from the back stack.
+ */
+ public void setEnterSharedElementCallback(SharedElementCallback callback) {
+ if (callback == null) {
+ if (mAnimationInfo == null) {
+ return; // already a null callback
+ }
+ callback = SharedElementCallback.NULL_CALLBACK;
+ }
+ ensureAnimationInfo().mEnterTransitionCallback = callback;
+ }
+
+ /**
+ * When custom transitions are used with Fragments, the exit transition callback
+ * is called when this Fragment is attached or detached when popping the back stack.
+ *
+ * @param callback Used to manipulate the shared element transitions on this Fragment
+ * when added as a pop from the back stack.
+ */
+ public void setExitSharedElementCallback(SharedElementCallback callback) {
+ if (callback == null) {
+ if (mAnimationInfo == null) {
+ return; // already a null callback
+ }
+ callback = SharedElementCallback.NULL_CALLBACK;
+ }
+ ensureAnimationInfo().mExitTransitionCallback = callback;
+ }
+
+ /**
+ * Sets the Transition that will be used to move Views into the initial scene. The entering
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected.
+ *
+ * @param transition The Transition to use to move Views into the initial Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentEnterTransition
+ */
+ public void setEnterTransition(Transition transition) {
+ if (shouldChangeTransition(transition, null)) {
+ ensureAnimationInfo().mEnterTransition = transition;
+ }
+ }
+
+ /**
+ * Returns the Transition that will be used to move Views into the initial scene. The entering
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#INVISIBLE} to {@link View#VISIBLE}.
+ *
+ * @return the Transition to use to move Views into the initial Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentEnterTransition
+ */
+ public Transition getEnterTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mEnterTransition;
+ }
+
+ /**
+ * Sets the Transition that will be used to move Views out of the scene when the Fragment is
+ * preparing to be removed, hidden, or detached because of popping the back stack. The exiting
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#VISIBLE} to {@link View#INVISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected. If nothing is set, the default will be to
+ * use the same value as set in {@link #setEnterTransition(android.transition.Transition)}.
+ *
+ * @param transition The Transition to use to move Views out of the Scene when the Fragment
+ * is preparing to close.
+ * @attr ref android.R.styleable#Fragment_fragmentExitTransition
+ */
+ public void setReturnTransition(Transition transition) {
+ if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) {
+ ensureAnimationInfo().mReturnTransition = transition;
+ }
+ }
+
+ /**
+ * Returns the Transition that will be used to move Views out of the scene when the Fragment is
+ * preparing to be removed, hidden, or detached because of popping the back stack. The exiting
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#VISIBLE} to {@link View#INVISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected.
+ *
+ * @return the Transition to use to move Views out of the Scene when the Fragment
+ * is preparing to close.
+ * @attr ref android.R.styleable#Fragment_fragmentExitTransition
+ */
+ public Transition getReturnTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition()
+ : mAnimationInfo.mReturnTransition;
+ }
+
+ /**
+ * Sets the Transition that will be used to move Views out of the scene when the
+ * fragment is removed, hidden, or detached when not popping the back stack.
+ * The exiting Views will be those that are regular Views or ViewGroups that
+ * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as exiting is governed by changing visibility
+ * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will
+ * remain unaffected.
+ *
+ * @param transition The Transition to use to move Views out of the Scene when the Fragment
+ * is being closed not due to popping the back stack.
+ * @attr ref android.R.styleable#Fragment_fragmentExitTransition
+ */
+ public void setExitTransition(Transition transition) {
+ if (shouldChangeTransition(transition, null)) {
+ ensureAnimationInfo().mExitTransition = transition;
+ }
+ }
+
+ /**
+ * Returns the Transition that will be used to move Views out of the scene when the
+ * fragment is removed, hidden, or detached when not popping the back stack.
+ * The exiting Views will be those that are regular Views or ViewGroups that
+ * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as exiting is governed by changing visibility
+ * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will
+ * remain unaffected.
+ *
+ * @return the Transition to use to move Views out of the Scene when the Fragment
+ * is being closed not due to popping the back stack.
+ * @attr ref android.R.styleable#Fragment_fragmentExitTransition
+ */
+ public Transition getExitTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mExitTransition;
+ }
+
+ /**
+ * Sets the Transition that will be used to move Views in to the scene when returning due
+ * to popping a back stack. The entering Views will be those that are regular Views
+ * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions
+ * will extend {@link android.transition.Visibility} as exiting is governed by changing
+ * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null,
+ * the views will remain unaffected. If nothing is set, the default will be to use the same
+ * transition as {@link #setExitTransition(android.transition.Transition)}.
+ *
+ * @param transition The Transition to use to move Views into the scene when reentering from a
+ * previously-started Activity.
+ * @attr ref android.R.styleable#Fragment_fragmentReenterTransition
+ */
+ public void setReenterTransition(Transition transition) {
+ if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) {
+ ensureAnimationInfo().mReenterTransition = transition;
+ }
+ }
+
+ /**
+ * Returns the Transition that will be used to move Views in to the scene when returning due
+ * to popping a back stack. The entering Views will be those that are regular Views
+ * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions
+ * will extend {@link android.transition.Visibility} as exiting is governed by changing
+ * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null,
+ * the views will remain unaffected. If nothing is set, the default will be to use the same
+ * transition as {@link #setExitTransition(android.transition.Transition)}.
+ *
+ * @return the Transition to use to move Views into the scene when reentering from a
+ * previously-started Activity.
+ * @attr ref android.R.styleable#Fragment_fragmentReenterTransition
+ */
+ public Transition getReenterTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition()
+ : mAnimationInfo.mReenterTransition;
+ }
+
+ /**
+ * Sets the Transition that will be used for shared elements transferred into the content
+ * Scene. Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ *
+ * @param transition The Transition to use for shared elements transferred into the content
+ * Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentSharedElementEnterTransition
+ */
+ public void setSharedElementEnterTransition(Transition transition) {
+ if (shouldChangeTransition(transition, null)) {
+ ensureAnimationInfo().mSharedElementEnterTransition = transition;
+ }
+ }
+
+ /**
+ * Returns the Transition that will be used for shared elements transferred into the content
+ * Scene. Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ *
+ * @return The Transition to use for shared elements transferred into the content
+ * Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentSharedElementEnterTransition
+ */
+ public Transition getSharedElementEnterTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mSharedElementEnterTransition;
+ }
+
+ /**
+ * Sets the Transition that will be used for shared elements transferred back during a
+ * pop of the back stack. This Transition acts in the leaving Fragment.
+ * Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ * If no value is set, the default will be to use the same value as
+ * {@link #setSharedElementEnterTransition(android.transition.Transition)}.
+ *
+ * @param transition The Transition to use for shared elements transferred out of the content
+ * Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentSharedElementReturnTransition
+ */
+ public void setSharedElementReturnTransition(Transition transition) {
+ if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) {
+ ensureAnimationInfo().mSharedElementReturnTransition = transition;
+ }
+ }
+
+ /**
+ * Return the Transition that will be used for shared elements transferred back during a
+ * pop of the back stack. This Transition acts in the leaving Fragment.
+ * Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ * If no value is set, the default will be to use the same value as
+ * {@link #setSharedElementEnterTransition(android.transition.Transition)}.
+ *
+ * @return The Transition to use for shared elements transferred out of the content
+ * Scene.
+ * @attr ref android.R.styleable#Fragment_fragmentSharedElementReturnTransition
+ */
+ public Transition getSharedElementReturnTransition() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mSharedElementReturnTransition == USE_DEFAULT_TRANSITION
+ ? getSharedElementEnterTransition()
+ : mAnimationInfo.mSharedElementReturnTransition;
+ }
+
+ /**
+ * Sets whether the the exit transition and enter transition overlap or not.
+ * When true, the enter transition will start as soon as possible. When false, the
+ * enter transition will wait until the exit transition completes before starting.
+ *
+ * @param allow true to start the enter transition when possible or false to
+ * wait until the exiting transition completes.
+ * @attr ref android.R.styleable#Fragment_fragmentAllowEnterTransitionOverlap
+ */
+ public void setAllowEnterTransitionOverlap(boolean allow) {
+ ensureAnimationInfo().mAllowEnterTransitionOverlap = allow;
+ }
+
+ /**
+ * Returns whether the the exit transition and enter transition overlap or not.
+ * When true, the enter transition will start as soon as possible. When false, the
+ * enter transition will wait until the exit transition completes before starting.
+ *
+ * @return true when the enter transition should start as soon as possible or false to
+ * when it should wait until the exiting transition completes.
+ * @attr ref android.R.styleable#Fragment_fragmentAllowEnterTransitionOverlap
+ */
+ public boolean getAllowEnterTransitionOverlap() {
+ return (mAnimationInfo == null || mAnimationInfo.mAllowEnterTransitionOverlap == null)
+ ? true : mAnimationInfo.mAllowEnterTransitionOverlap;
+ }
+
+ /**
+ * Sets whether the the return transition and reenter transition overlap or not.
+ * When true, the reenter transition will start as soon as possible. When false, the
+ * reenter transition will wait until the return transition completes before starting.
+ *
+ * @param allow true to start the reenter transition when possible or false to wait until the
+ * return transition completes.
+ * @attr ref android.R.styleable#Fragment_fragmentAllowReturnTransitionOverlap
+ */
+ public void setAllowReturnTransitionOverlap(boolean allow) {
+ ensureAnimationInfo().mAllowReturnTransitionOverlap = allow;
+ }
+
+ /**
+ * Returns whether the the return transition and reenter transition overlap or not.
+ * When true, the reenter transition will start as soon as possible. When false, the
+ * reenter transition will wait until the return transition completes before starting.
+ *
+ * @return true to start the reenter transition when possible or false to wait until the
+ * return transition completes.
+ * @attr ref android.R.styleable#Fragment_fragmentAllowReturnTransitionOverlap
+ */
+ public boolean getAllowReturnTransitionOverlap() {
+ return (mAnimationInfo == null || mAnimationInfo.mAllowReturnTransitionOverlap == null)
+ ? true : mAnimationInfo.mAllowReturnTransitionOverlap;
+ }
+
+ /**
+ * Postpone the entering Fragment transition until {@link #startPostponedEnterTransition()}
+ * or {@link FragmentManager#executePendingTransactions()} has been called.
+ * <p>
+ * This method gives the Fragment the ability to delay Fragment animations
+ * until all data is loaded. Until then, the added, shown, and
+ * attached Fragments will be INVISIBLE and removed, hidden, and detached Fragments won't
+ * be have their Views removed. The transaction runs when all postponed added Fragments in the
+ * transaction have called {@link #startPostponedEnterTransition()}.
+ * <p>
+ * This method should be called before being added to the FragmentTransaction or
+ * in {@link #onCreate(Bundle), {@link #onAttach(Context)}, or
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}.
+ * {@link #startPostponedEnterTransition()} must be called to allow the Fragment to
+ * start the transitions.
+ * <p>
+ * When a FragmentTransaction is started that may affect a postponed FragmentTransaction,
+ * based on which containers are in their operations, the postponed FragmentTransaction
+ * will have its start triggered. The early triggering may result in faulty or nonexistent
+ * animations in the postponed transaction. FragmentTransactions that operate only on
+ * independent containers will not interfere with each other's postponement.
+ * <p>
+ * Calling postponeEnterTransition on Fragments with a null View will not postpone the
+ * transition. Likewise, postponement only works if FragmentTransaction optimizations are
+ * enabled.
+ *
+ * @see Activity#postponeEnterTransition()
+ * @see FragmentTransaction#setReorderingAllowed(boolean)
+ */
+ public void postponeEnterTransition() {
+ ensureAnimationInfo().mEnterTransitionPostponed = true;
+ }
+
+ /**
+ * Begin postponed transitions after {@link #postponeEnterTransition()} was called.
+ * If postponeEnterTransition() was called, you must call startPostponedEnterTransition()
+ * or {@link FragmentManager#executePendingTransactions()} to complete the FragmentTransaction.
+ * If postponement was interrupted with {@link FragmentManager#executePendingTransactions()},
+ * before {@code startPostponedEnterTransition()}, animations may not run or may execute
+ * improperly.
+ *
+ * @see Activity#startPostponedEnterTransition()
+ */
+ public void startPostponedEnterTransition() {
+ if (mFragmentManager == null || mFragmentManager.mHost == null) {
+ ensureAnimationInfo().mEnterTransitionPostponed = false;
+ } else if (Looper.myLooper() != mFragmentManager.mHost.getHandler().getLooper()) {
+ mFragmentManager.mHost.getHandler().
+ postAtFrontOfQueue(this::callStartTransitionListener);
+ } else {
+ callStartTransitionListener();
+ }
+ }
+
+ /**
+ * Calls the start transition listener. This must be called on the UI thread.
+ */
+ private void callStartTransitionListener() {
+ final OnStartEnterTransitionListener listener;
+ if (mAnimationInfo == null) {
+ listener = null;
+ } else {
+ mAnimationInfo.mEnterTransitionPostponed = false;
+ listener = mAnimationInfo.mStartEnterTransitionListener;
+ mAnimationInfo.mStartEnterTransitionListener = null;
+ }
+ if (listener != null) {
+ listener.onStartEnterTransition();
+ }
+ }
+
+ /**
+ * Returns true if mAnimationInfo is not null or the transition differs from the default value.
+ * This is broken out to ensure mAnimationInfo is properly locked when checking.
+ */
+ private boolean shouldChangeTransition(Transition transition, Transition defaultValue) {
+ if (transition == defaultValue) {
+ return mAnimationInfo != null;
+ }
+ return true;
+ }
+
+ /**
+ * Print the Fragments's state into the given stream.
+ *
+ * @param prefix Text to print at the front of each line.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mFragmentId=#");
+ writer.print(Integer.toHexString(mFragmentId));
+ writer.print(" mContainerId=#");
+ writer.print(Integer.toHexString(mContainerId));
+ writer.print(" mTag="); writer.println(mTag);
+ writer.print(prefix); writer.print("mState="); writer.print(mState);
+ writer.print(" mIndex="); writer.print(mIndex);
+ writer.print(" mWho="); writer.print(mWho);
+ writer.print(" mBackStackNesting="); writer.println(mBackStackNesting);
+ writer.print(prefix); writer.print("mAdded="); writer.print(mAdded);
+ writer.print(" mRemoving="); writer.print(mRemoving);
+ writer.print(" mFromLayout="); writer.print(mFromLayout);
+ writer.print(" mInLayout="); writer.println(mInLayout);
+ writer.print(prefix); writer.print("mHidden="); writer.print(mHidden);
+ writer.print(" mDetached="); writer.print(mDetached);
+ writer.print(" mMenuVisible="); writer.print(mMenuVisible);
+ writer.print(" mHasMenu="); writer.println(mHasMenu);
+ writer.print(prefix); writer.print("mRetainInstance="); writer.print(mRetainInstance);
+ writer.print(" mRetaining="); writer.print(mRetaining);
+ writer.print(" mUserVisibleHint="); writer.println(mUserVisibleHint);
+ if (mFragmentManager != null) {
+ writer.print(prefix); writer.print("mFragmentManager=");
+ writer.println(mFragmentManager);
+ }
+ if (mHost != null) {
+ writer.print(prefix); writer.print("mHost=");
+ writer.println(mHost);
+ }
+ if (mParentFragment != null) {
+ writer.print(prefix); writer.print("mParentFragment=");
+ writer.println(mParentFragment);
+ }
+ if (mArguments != null) {
+ writer.print(prefix); writer.print("mArguments="); writer.println(mArguments);
+ }
+ if (mSavedFragmentState != null) {
+ writer.print(prefix); writer.print("mSavedFragmentState=");
+ writer.println(mSavedFragmentState);
+ }
+ if (mSavedViewState != null) {
+ writer.print(prefix); writer.print("mSavedViewState=");
+ writer.println(mSavedViewState);
+ }
+ if (mTarget != null) {
+ writer.print(prefix); writer.print("mTarget="); writer.print(mTarget);
+ writer.print(" mTargetRequestCode=");
+ writer.println(mTargetRequestCode);
+ }
+ if (getNextAnim() != 0) {
+ writer.print(prefix); writer.print("mNextAnim="); writer.println(getNextAnim());
+ }
+ if (mContainer != null) {
+ writer.print(prefix); writer.print("mContainer="); writer.println(mContainer);
+ }
+ if (mView != null) {
+ writer.print(prefix); writer.print("mView="); writer.println(mView);
+ }
+ if (getAnimatingAway() != null) {
+ writer.print(prefix); writer.print("mAnimatingAway=");
+ writer.println(getAnimatingAway());
+ writer.print(prefix); writer.print("mStateAfterAnimating=");
+ writer.println(getStateAfterAnimating());
+ }
+ if (mLoaderManager != null) {
+ writer.print(prefix); writer.println("Loader Manager:");
+ mLoaderManager.dump(prefix + " ", fd, writer, args);
+ }
+ if (mChildFragmentManager != null) {
+ writer.print(prefix); writer.println("Child " + mChildFragmentManager + ":");
+ mChildFragmentManager.dump(prefix + " ", fd, writer, args);
+ }
+ }
+
+ Fragment findFragmentByWho(String who) {
+ if (who.equals(mWho)) {
+ return this;
+ }
+ if (mChildFragmentManager != null) {
+ return mChildFragmentManager.findFragmentByWho(who);
+ }
+ return null;
+ }
+
+ void instantiateChildFragmentManager() {
+ mChildFragmentManager = new FragmentManagerImpl();
+ mChildFragmentManager.attachController(mHost, new FragmentContainer() {
+ @Override
+ @Nullable
+ public <T extends View> T onFindViewById(int id) {
+ if (mView == null) {
+ throw new IllegalStateException("Fragment does not have a view");
+ }
+ return mView.findViewById(id);
+ }
+
+ @Override
+ public boolean onHasView() {
+ return (mView != null);
+ }
+ }, this);
+ }
+
+ void performCreate(Bundle savedInstanceState) {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ }
+ mState = CREATED;
+ mCalled = false;
+ onCreate(savedInstanceState);
+ mIsCreated = true;
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onCreate()");
+ }
+ final Context context = getContext();
+ final int version = context != null ? context.getApplicationInfo().targetSdkVersion : 0;
+ if (version < Build.VERSION_CODES.N) {
+ restoreChildFragmentState(savedInstanceState, false);
+ }
+ }
+
+ View performCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ }
+ mPerformedCreateView = true;
+ return onCreateView(inflater, container, savedInstanceState);
+ }
+
+ void performActivityCreated(Bundle savedInstanceState) {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ }
+ mState = ACTIVITY_CREATED;
+ mCalled = false;
+ onActivityCreated(savedInstanceState);
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onActivityCreated()");
+ }
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchActivityCreated();
+ }
+ }
+
+ void performStart() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ mChildFragmentManager.execPendingActions();
+ }
+ mState = STARTED;
+ mCalled = false;
+ onStart();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onStart()");
+ }
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchStart();
+ }
+ if (mLoaderManager != null) {
+ mLoaderManager.doReportStart();
+ }
+ }
+
+ void performResume() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ mChildFragmentManager.execPendingActions();
+ }
+ mState = RESUMED;
+ mCalled = false;
+ onResume();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onResume()");
+ }
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchResume();
+ mChildFragmentManager.execPendingActions();
+ }
+ }
+
+ void noteStateNotSaved() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.noteStateNotSaved();
+ }
+ }
+
+ @Deprecated
+ void performMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ onMultiWindowModeChanged(isInMultiWindowMode);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);
+ }
+ }
+
+ void performMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
+ onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+ }
+
+ @Deprecated
+ void performPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ onPictureInPictureModeChanged(isInPictureInPictureMode);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+ }
+
+ void performPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode,
+ newConfig);
+ }
+ }
+
+ void performConfigurationChanged(Configuration newConfig) {
+ onConfigurationChanged(newConfig);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchConfigurationChanged(newConfig);
+ }
+ }
+
+ void performLowMemory() {
+ onLowMemory();
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchLowMemory();
+ }
+ }
+
+ void performTrimMemory(int level) {
+ onTrimMemory(level);
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchTrimMemory(level);
+ }
+ }
+
+ boolean performCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ boolean show = false;
+ if (!mHidden) {
+ if (mHasMenu && mMenuVisible) {
+ show = true;
+ onCreateOptionsMenu(menu, inflater);
+ }
+ if (mChildFragmentManager != null) {
+ show |= mChildFragmentManager.dispatchCreateOptionsMenu(menu, inflater);
+ }
+ }
+ return show;
+ }
+
+ boolean performPrepareOptionsMenu(Menu menu) {
+ boolean show = false;
+ if (!mHidden) {
+ if (mHasMenu && mMenuVisible) {
+ show = true;
+ onPrepareOptionsMenu(menu);
+ }
+ if (mChildFragmentManager != null) {
+ show |= mChildFragmentManager.dispatchPrepareOptionsMenu(menu);
+ }
+ }
+ return show;
+ }
+
+ boolean performOptionsItemSelected(MenuItem item) {
+ if (!mHidden) {
+ if (mHasMenu && mMenuVisible) {
+ if (onOptionsItemSelected(item)) {
+ return true;
+ }
+ }
+ if (mChildFragmentManager != null) {
+ if (mChildFragmentManager.dispatchOptionsItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean performContextItemSelected(MenuItem item) {
+ if (!mHidden) {
+ if (onContextItemSelected(item)) {
+ return true;
+ }
+ if (mChildFragmentManager != null) {
+ if (mChildFragmentManager.dispatchContextItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ void performOptionsMenuClosed(Menu menu) {
+ if (!mHidden) {
+ if (mHasMenu && mMenuVisible) {
+ onOptionsMenuClosed(menu);
+ }
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchOptionsMenuClosed(menu);
+ }
+ }
+ }
+
+ void performSaveInstanceState(Bundle outState) {
+ onSaveInstanceState(outState);
+ if (mChildFragmentManager != null) {
+ Parcelable p = mChildFragmentManager.saveAllState();
+ if (p != null) {
+ outState.putParcelable(Activity.FRAGMENTS_TAG, p);
+ }
+ }
+ }
+
+ void performPause() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchPause();
+ }
+ mState = STARTED;
+ mCalled = false;
+ onPause();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onPause()");
+ }
+ }
+
+ void performStop() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchStop();
+ }
+ mState = STOPPED;
+ mCalled = false;
+ onStop();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onStop()");
+ }
+
+ if (mLoadersStarted) {
+ mLoadersStarted = false;
+ if (!mCheckedForLoaderManager) {
+ mCheckedForLoaderManager = true;
+ mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, false);
+ }
+ if (mLoaderManager != null) {
+ if (mHost.getRetainLoaders()) {
+ mLoaderManager.doRetain();
+ } else {
+ mLoaderManager.doStop();
+ }
+ }
+ }
+ }
+
+ void performDestroyView() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchDestroyView();
+ }
+ mState = CREATED;
+ mCalled = false;
+ onDestroyView();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onDestroyView()");
+ }
+ if (mLoaderManager != null) {
+ mLoaderManager.doReportNextStart();
+ }
+ mPerformedCreateView = false;
+ }
+
+ void performDestroy() {
+ if (mChildFragmentManager != null) {
+ mChildFragmentManager.dispatchDestroy();
+ }
+ mState = INITIALIZING;
+ mCalled = false;
+ mIsCreated = false;
+ onDestroy();
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onDestroy()");
+ }
+ mChildFragmentManager = null;
+ }
+
+ void performDetach() {
+ mCalled = false;
+ onDetach();
+ mLayoutInflater = null;
+ if (!mCalled) {
+ throw new SuperNotCalledException("Fragment " + this
+ + " did not call through to super.onDetach()");
+ }
+
+ // Destroy the child FragmentManager if we still have it here.
+ // We won't unless we're retaining our instance and if we do,
+ // our child FragmentManager instance state will have already been saved.
+ if (mChildFragmentManager != null) {
+ if (!mRetaining) {
+ throw new IllegalStateException("Child FragmentManager of " + this + " was not "
+ + " destroyed and this fragment is not retaining instance");
+ }
+ mChildFragmentManager.dispatchDestroy();
+ mChildFragmentManager = null;
+ }
+ }
+
+ void setOnStartEnterTransitionListener(OnStartEnterTransitionListener listener) {
+ ensureAnimationInfo();
+ if (listener == mAnimationInfo.mStartEnterTransitionListener) {
+ return;
+ }
+ if (listener != null && mAnimationInfo.mStartEnterTransitionListener != null) {
+ throw new IllegalStateException("Trying to set a replacement " +
+ "startPostponedEnterTransition on " + this);
+ }
+ if (mAnimationInfo.mEnterTransitionPostponed) {
+ mAnimationInfo.mStartEnterTransitionListener = listener;
+ }
+ if (listener != null) {
+ listener.startListening();
+ }
+ }
+
+ private static Transition loadTransition(Context context, TypedArray typedArray,
+ Transition currentValue, Transition defaultValue, int id) {
+ if (currentValue != defaultValue) {
+ return currentValue;
+ }
+ int transitionId = typedArray.getResourceId(id, 0);
+ Transition transition = defaultValue;
+ if (transitionId != 0 && transitionId != com.android.internal.R.transition.no_transition) {
+ TransitionInflater inflater = TransitionInflater.from(context);
+ transition = inflater.inflateTransition(transitionId);
+ if (transition instanceof TransitionSet &&
+ ((TransitionSet)transition).getTransitionCount() == 0) {
+ transition = null;
+ }
+ }
+ return transition;
+ }
+
+ private AnimationInfo ensureAnimationInfo() {
+ if (mAnimationInfo == null) {
+ mAnimationInfo = new AnimationInfo();
+ }
+ return mAnimationInfo;
+ }
+
+ int getNextAnim() {
+ if (mAnimationInfo == null) {
+ return 0;
+ }
+ return mAnimationInfo.mNextAnim;
+ }
+
+ void setNextAnim(int animResourceId) {
+ if (mAnimationInfo == null && animResourceId == 0) {
+ return; // no change!
+ }
+ ensureAnimationInfo().mNextAnim = animResourceId;
+ }
+
+ int getNextTransition() {
+ if (mAnimationInfo == null) {
+ return 0;
+ }
+ return mAnimationInfo.mNextTransition;
+ }
+
+ void setNextTransition(int nextTransition, int nextTransitionStyle) {
+ if (mAnimationInfo == null && nextTransition == 0 && nextTransitionStyle == 0) {
+ return; // no change!
+ }
+ ensureAnimationInfo();
+ mAnimationInfo.mNextTransition = nextTransition;
+ mAnimationInfo.mNextTransitionStyle = nextTransitionStyle;
+ }
+
+ int getNextTransitionStyle() {
+ if (mAnimationInfo == null) {
+ return 0;
+ }
+ return mAnimationInfo.mNextTransitionStyle;
+ }
+
+ SharedElementCallback getEnterTransitionCallback() {
+ if (mAnimationInfo == null) {
+ return SharedElementCallback.NULL_CALLBACK;
+ }
+ return mAnimationInfo.mEnterTransitionCallback;
+ }
+
+ SharedElementCallback getExitTransitionCallback() {
+ if (mAnimationInfo == null) {
+ return SharedElementCallback.NULL_CALLBACK;
+ }
+ return mAnimationInfo.mExitTransitionCallback;
+ }
+
+ Animator getAnimatingAway() {
+ if (mAnimationInfo == null) {
+ return null;
+ }
+ return mAnimationInfo.mAnimatingAway;
+ }
+
+ void setAnimatingAway(Animator animator) {
+ ensureAnimationInfo().mAnimatingAway = animator;
+ }
+
+ int getStateAfterAnimating() {
+ if (mAnimationInfo == null) {
+ return 0;
+ }
+ return mAnimationInfo.mStateAfterAnimating;
+ }
+
+ void setStateAfterAnimating(int state) {
+ ensureAnimationInfo().mStateAfterAnimating = state;
+ }
+
+ boolean isPostponed() {
+ if (mAnimationInfo == null) {
+ return false;
+ }
+ return mAnimationInfo.mEnterTransitionPostponed;
+ }
+
+ boolean isHideReplaced() {
+ if (mAnimationInfo == null) {
+ return false;
+ }
+ return mAnimationInfo.mIsHideReplaced;
+ }
+
+ void setHideReplaced(boolean replaced) {
+ ensureAnimationInfo().mIsHideReplaced = replaced;
+ }
+
+ /**
+ * Used internally to be notified when {@link #startPostponedEnterTransition()} has
+ * been called. This listener will only be called once and then be removed from the
+ * listeners.
+ */
+ interface OnStartEnterTransitionListener {
+ void onStartEnterTransition();
+ void startListening();
+ }
+
+ /**
+ * Contains all the animation and transition information for a fragment. This will only
+ * be instantiated for Fragments that have Views.
+ */
+ static class AnimationInfo {
+ // Non-null if the fragment's view hierarchy is currently animating away,
+ // meaning we need to wait a bit on completely destroying it. This is the
+ // animation that is running.
+ Animator mAnimatingAway;
+
+ // If mAnimatingAway != null, this is the state we should move to once the
+ // animation is done.
+ int mStateAfterAnimating;
+
+ // If app has requested a specific animation, this is the one to use.
+ int mNextAnim;
+
+ // If app has requested a specific transition, this is the one to use.
+ int mNextTransition;
+
+ // If app has requested a specific transition style, this is the one to use.
+ int mNextTransitionStyle;
+
+ private Transition mEnterTransition = null;
+ private Transition mReturnTransition = USE_DEFAULT_TRANSITION;
+ private Transition mExitTransition = null;
+ private Transition mReenterTransition = USE_DEFAULT_TRANSITION;
+ private Transition mSharedElementEnterTransition = null;
+ private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION;
+ private Boolean mAllowReturnTransitionOverlap;
+ private Boolean mAllowEnterTransitionOverlap;
+
+ SharedElementCallback mEnterTransitionCallback = SharedElementCallback.NULL_CALLBACK;
+ SharedElementCallback mExitTransitionCallback = SharedElementCallback.NULL_CALLBACK;
+
+ // True when postponeEnterTransition has been called and startPostponeEnterTransition
+ // hasn't been called yet.
+ boolean mEnterTransitionPostponed;
+
+ // Listener to wait for startPostponeEnterTransition. After being called, it will
+ // be set to null
+ OnStartEnterTransitionListener mStartEnterTransitionListener;
+
+ // True if the View was hidden, but the transition is handling the hide
+ boolean mIsHideReplaced;
+ }
+}
diff --git a/android/app/FragmentBreadCrumbs.java b/android/app/FragmentBreadCrumbs.java
new file mode 100644
index 00000000..d0aa0fd6
--- /dev/null
+++ b/android/app/FragmentBreadCrumbs.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.animation.LayoutTransition;
+import android.app.FragmentManager.BackStackEntry;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Helper class for showing "bread crumbs" representing the fragment
+ * stack in an activity. This is intended to be used with
+ * {@link ActionBar#setCustomView(View)
+ * ActionBar.setCustomView(View)} to place the bread crumbs in
+ * the action bar.
+ *
+ * <p>The default style for this view is
+ * {@link android.R.style#Widget_FragmentBreadCrumbs}.
+ *
+ * @deprecated This widget is no longer supported.
+ */
+@Deprecated
+public class FragmentBreadCrumbs extends ViewGroup
+ implements FragmentManager.OnBackStackChangedListener {
+ Activity mActivity;
+ LayoutInflater mInflater;
+ LinearLayout mContainer;
+ int mMaxVisible = -1;
+
+ // Hahah
+ BackStackRecord mTopEntry;
+ BackStackRecord mParentEntry;
+
+ /** Listener to inform when a parent entry is clicked */
+ private OnClickListener mParentClickListener;
+
+ private OnBreadCrumbClickListener mOnBreadCrumbClickListener;
+
+ private int mGravity;
+ private int mLayoutResId;
+ private int mTextColor;
+
+ private static final int DEFAULT_GRAVITY = Gravity.START | Gravity.CENTER_VERTICAL;
+
+ /**
+ * Interface to intercept clicks on the bread crumbs.
+ */
+ public interface OnBreadCrumbClickListener {
+ /**
+ * Called when a bread crumb is clicked.
+ *
+ * @param backStack The BackStackEntry whose bread crumb was clicked.
+ * May be null, if this bread crumb is for the root of the back stack.
+ * @param flags Additional information about the entry. Currently
+ * always 0.
+ *
+ * @return Return true to consume this click. Return to false to allow
+ * the default action (popping back stack to this entry) to occur.
+ */
+ public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
+ }
+
+ public FragmentBreadCrumbs(Context context) {
+ this(context, null);
+ }
+
+ public FragmentBreadCrumbs(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle);
+ }
+
+ public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * @hide
+ */
+ public FragmentBreadCrumbs(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes);
+
+ mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity,
+ DEFAULT_GRAVITY);
+ mLayoutResId = a.getResourceId(
+ com.android.internal.R.styleable.FragmentBreadCrumbs_itemLayout,
+ com.android.internal.R.layout.fragment_bread_crumb_item);
+ mTextColor = a.getColor(
+ com.android.internal.R.styleable.FragmentBreadCrumbs_itemColor,
+ 0);
+
+ a.recycle();
+ }
+
+ /**
+ * Attach the bread crumbs to their activity. This must be called once
+ * when creating the bread crumbs.
+ */
+ public void setActivity(Activity a) {
+ mActivity = a;
+ mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContainer = (LinearLayout)mInflater.inflate(
+ com.android.internal.R.layout.fragment_bread_crumbs,
+ this, false);
+ addView(mContainer);
+ a.getFragmentManager().addOnBackStackChangedListener(this);
+ updateCrumbs();
+ setLayoutTransition(new LayoutTransition());
+ }
+
+ /**
+ * The maximum number of breadcrumbs to show. Older fragment headers will be hidden from view.
+ * @param visibleCrumbs the number of visible breadcrumbs. This should be greater than zero.
+ */
+ public void setMaxVisible(int visibleCrumbs) {
+ if (visibleCrumbs < 1) {
+ throw new IllegalArgumentException("visibleCrumbs must be greater than zero");
+ }
+ mMaxVisible = visibleCrumbs;
+ }
+
+ /**
+ * Inserts an optional parent entry at the first position in the breadcrumbs. Selecting this
+ * entry will result in a call to the specified listener's
+ * {@link android.view.View.OnClickListener#onClick(View)}
+ * method.
+ *
+ * @param title the title for the parent entry
+ * @param shortTitle the short title for the parent entry
+ * @param listener the {@link android.view.View.OnClickListener} to be called when clicked.
+ * A null will result in no action being taken when the parent entry is clicked.
+ */
+ public void setParentTitle(CharSequence title, CharSequence shortTitle,
+ OnClickListener listener) {
+ mParentEntry = createBackStackEntry(title, shortTitle);
+ mParentClickListener = listener;
+ updateCrumbs();
+ }
+
+ /**
+ * Sets a listener for clicks on the bread crumbs. This will be called before
+ * the default click action is performed.
+ *
+ * @param listener The new listener to set. Replaces any existing listener.
+ */
+ public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
+ mOnBreadCrumbClickListener = listener;
+ }
+
+ private BackStackRecord createBackStackEntry(CharSequence title, CharSequence shortTitle) {
+ if (title == null) return null;
+
+ final BackStackRecord entry = new BackStackRecord(
+ (FragmentManagerImpl) mActivity.getFragmentManager());
+ entry.setBreadCrumbTitle(title);
+ entry.setBreadCrumbShortTitle(shortTitle);
+ return entry;
+ }
+
+ /**
+ * Set a custom title for the bread crumbs. This will be the first entry
+ * shown at the left, representing the root of the bread crumbs. If the
+ * title is null, it will not be shown.
+ */
+ public void setTitle(CharSequence title, CharSequence shortTitle) {
+ mTopEntry = createBackStackEntry(title, shortTitle);
+ updateCrumbs();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Eventually we should implement our own layout of the views, rather than relying on
+ // a single linear layout.
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return;
+ }
+
+ final View child = getChildAt(0);
+
+ final int childTop = mPaddingTop;
+ final int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom;
+
+ int childLeft;
+ int childRight;
+
+ final int layoutDirection = getLayoutDirection();
+ final int horizontalGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ switch (Gravity.getAbsoluteGravity(horizontalGravity, layoutDirection)) {
+ case Gravity.RIGHT:
+ childRight = mRight - mLeft - mPaddingRight;
+ childLeft = childRight - child.getMeasuredWidth();
+ break;
+
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = mPaddingLeft + (mRight - mLeft - child.getMeasuredWidth()) / 2;
+ childRight = childLeft + child.getMeasuredWidth();
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = mPaddingLeft;
+ childRight = childLeft + child.getMeasuredWidth();
+ break;
+ }
+
+ if (childLeft < mPaddingLeft) {
+ childLeft = mPaddingLeft;
+ }
+
+ if (childRight > mRight - mLeft - mPaddingRight) {
+ childRight = mRight - mLeft - mPaddingRight;
+ }
+
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int count = getChildCount();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int measuredChildState = 0;
+
+ // Find rightmost and bottom-most child
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
+ measuredChildState = combineMeasuredStates(measuredChildState,
+ child.getMeasuredState());
+ }
+ }
+
+ // Account for padding too
+ maxWidth += mPaddingLeft + mPaddingRight;
+ maxHeight += mPaddingTop + mPaddingBottom;
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ updateCrumbs();
+ }
+
+ /**
+ * Returns the number of entries before the backstack, including the title of the current
+ * fragment and any custom parent title that was set.
+ */
+ private int getPreEntryCount() {
+ return (mTopEntry != null ? 1 : 0) + (mParentEntry != null ? 1 : 0);
+ }
+
+ /**
+ * Returns the pre-entry corresponding to the index. If there is a parent and a top entry
+ * set, parent has an index of zero and top entry has an index of 1. Returns null if the
+ * specified index doesn't exist or is null.
+ * @param index should not be more than {@link #getPreEntryCount()} - 1
+ */
+ private BackStackEntry getPreEntry(int index) {
+ // If there's a parent entry, then return that for zero'th item, else top entry.
+ if (mParentEntry != null) {
+ return index == 0 ? mParentEntry : mTopEntry;
+ } else {
+ return mTopEntry;
+ }
+ }
+
+ void updateCrumbs() {
+ FragmentManager fm = mActivity.getFragmentManager();
+ int numEntries = fm.getBackStackEntryCount();
+ int numPreEntries = getPreEntryCount();
+ int numViews = mContainer.getChildCount();
+ for (int i = 0; i < numEntries + numPreEntries; i++) {
+ BackStackEntry bse = i < numPreEntries
+ ? getPreEntry(i)
+ : fm.getBackStackEntryAt(i - numPreEntries);
+ if (i < numViews) {
+ View v = mContainer.getChildAt(i);
+ Object tag = v.getTag();
+ if (tag != bse) {
+ for (int j = i; j < numViews; j++) {
+ mContainer.removeViewAt(i);
+ }
+ numViews = i;
+ }
+ }
+ if (i >= numViews) {
+ final View item = mInflater.inflate(mLayoutResId, this, false);
+ final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title);
+ text.setText(bse.getBreadCrumbTitle());
+ text.setTag(bse);
+ text.setTextColor(mTextColor);
+ if (i == 0) {
+ item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE);
+ }
+ mContainer.addView(item);
+ text.setOnClickListener(mOnClickListener);
+ }
+ }
+ int viewI = numEntries + numPreEntries;
+ numViews = mContainer.getChildCount();
+ while (numViews > viewI) {
+ mContainer.removeViewAt(numViews - 1);
+ numViews--;
+ }
+ // Adjust the visibility and availability of the bread crumbs and divider
+ for (int i = 0; i < numViews; i++) {
+ final View child = mContainer.getChildAt(i);
+ // Disable the last one
+ child.findViewById(com.android.internal.R.id.title).setEnabled(i < numViews - 1);
+ if (mMaxVisible > 0) {
+ // Make only the last mMaxVisible crumbs visible
+ child.setVisibility(i < numViews - mMaxVisible ? View.GONE : View.VISIBLE);
+ final View leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
+ // Remove the divider for all but the last mMaxVisible - 1
+ leftIcon.setVisibility(i > numViews - mMaxVisible && i != 0 ? View.VISIBLE
+ : View.GONE);
+ }
+ }
+ }
+
+ private OnClickListener mOnClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (v.getTag() instanceof BackStackEntry) {
+ BackStackEntry bse = (BackStackEntry) v.getTag();
+ if (bse == mParentEntry) {
+ if (mParentClickListener != null) {
+ mParentClickListener.onClick(v);
+ }
+ } else {
+ if (mOnBreadCrumbClickListener != null) {
+ if (mOnBreadCrumbClickListener.onBreadCrumbClick(
+ bse == mTopEntry ? null : bse, 0)) {
+ return;
+ }
+ }
+ if (bse == mTopEntry) {
+ // Pop everything off the back stack.
+ mActivity.getFragmentManager().popBackStack();
+ } else {
+ mActivity.getFragmentManager().popBackStack(bse.getId(), 0);
+ }
+ }
+ }
+ }
+ };
+}
diff --git a/android/app/FragmentContainer.java b/android/app/FragmentContainer.java
new file mode 100644
index 00000000..f8836bc8
--- /dev/null
+++ b/android/app/FragmentContainer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IdRes;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * Callbacks to a {@link Fragment}'s container.
+ */
+public abstract class FragmentContainer {
+ /**
+ * Return the view with the given resource ID. May return {@code null} if the
+ * view is not a child of this container.
+ */
+ @Nullable
+ public abstract <T extends View> T onFindViewById(@IdRes int id);
+
+ /**
+ * Return {@code true} if the container holds any view.
+ */
+ public abstract boolean onHasView();
+
+ /**
+ * Creates an instance of the specified fragment, can be overridden to construct fragments
+ * with dependencies, or change the fragment being constructed. By default just calls
+ * {@link Fragment#instantiate(Context, String, Bundle)}.
+ *
+ * @hide
+ */
+ public Fragment instantiate(Context context, String className, Bundle arguments) {
+ return Fragment.instantiate(context, className, arguments);
+ }
+}
diff --git a/android/app/FragmentController.java b/android/app/FragmentController.java
new file mode 100644
index 00000000..cff94d8c
--- /dev/null
+++ b/android/app/FragmentController.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Provides integration points with a {@link FragmentManager} for a fragment host.
+ * <p>
+ * It is the responsibility of the host to take care of the Fragment's lifecycle.
+ * The methods provided by {@link FragmentController} are for that purpose.
+ */
+public class FragmentController {
+ private final FragmentHostCallback<?> mHost;
+
+ /**
+ * Returns a {@link FragmentController}.
+ */
+ public static final FragmentController createController(FragmentHostCallback<?> callbacks) {
+ return new FragmentController(callbacks);
+ }
+
+ private FragmentController(FragmentHostCallback<?> callbacks) {
+ mHost = callbacks;
+ }
+
+ /**
+ * Returns a {@link FragmentManager} for this controller.
+ */
+ public FragmentManager getFragmentManager() {
+ return mHost.getFragmentManagerImpl();
+ }
+
+ /**
+ * Returns a {@link LoaderManager}.
+ */
+ public LoaderManager getLoaderManager() {
+ return mHost.getLoaderManagerImpl();
+ }
+
+ /**
+ * Returns a fragment with the given identifier.
+ */
+ @Nullable
+ public Fragment findFragmentByWho(String who) {
+ return mHost.mFragmentManager.findFragmentByWho(who);
+ }
+
+ /**
+ * Attaches the host to the FragmentManager for this controller. The host must be
+ * attached before the FragmentManager can be used to manage Fragments.
+ * */
+ public void attachHost(Fragment parent) {
+ mHost.mFragmentManager.attachController(
+ mHost, mHost /*container*/, parent);
+ }
+
+ /**
+ * Instantiates a Fragment's view.
+ *
+ * @param parent The parent that the created view will be placed
+ * in; <em>note that this may be null</em>.
+ * @param name Tag name to be inflated.
+ * @param context The context the view is being created in.
+ * @param attrs Inflation attributes as specified in XML file.
+ *
+ * @return view the newly created view
+ */
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ return mHost.mFragmentManager.onCreateView(parent, name, context, attrs);
+ }
+
+ /**
+ * Marks the fragment state as unsaved. This allows for "state loss" detection.
+ */
+ public void noteStateNotSaved() {
+ mHost.mFragmentManager.noteStateNotSaved();
+ }
+
+ /**
+ * Saves the state for all Fragments.
+ */
+ public Parcelable saveAllState() {
+ return mHost.mFragmentManager.saveAllState();
+ }
+
+ /**
+ * Restores the saved state for all Fragments. The given Fragment list are Fragment
+ * instances retained across configuration changes.
+ *
+ * @see #retainNonConfig()
+ *
+ * @deprecated use {@link #restoreAllState(Parcelable, FragmentManagerNonConfig)}
+ */
+ @Deprecated
+ public void restoreAllState(Parcelable state, List<Fragment> nonConfigList) {
+ mHost.mFragmentManager.restoreAllState(state,
+ new FragmentManagerNonConfig(nonConfigList, null));
+ }
+
+ /**
+ * Restores the saved state for all Fragments. The given FragmentManagerNonConfig are Fragment
+ * instances retained across configuration changes, including nested fragments
+ *
+ * @see #retainNestedNonConfig()
+ */
+ public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
+ mHost.mFragmentManager.restoreAllState(state, nonConfig);
+ }
+
+ /**
+ * Returns a list of Fragments that have opted to retain their instance across
+ * configuration changes.
+ *
+ * @deprecated use {@link #retainNestedNonConfig()} to also track retained
+ * nested child fragments
+ */
+ @Deprecated
+ public List<Fragment> retainNonConfig() {
+ return mHost.mFragmentManager.retainNonConfig().getFragments();
+ }
+
+ /**
+ * Returns a nested tree of Fragments that have opted to retain their instance across
+ * configuration changes.
+ */
+ public FragmentManagerNonConfig retainNestedNonConfig() {
+ return mHost.mFragmentManager.retainNonConfig();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the create state.
+ * <p>Call when Fragments should be created.
+ *
+ * @see Fragment#onCreate(Bundle)
+ */
+ public void dispatchCreate() {
+ mHost.mFragmentManager.dispatchCreate();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the activity created state.
+ * <p>Call when Fragments should be informed their host has been created.
+ *
+ * @see Fragment#onActivityCreated(Bundle)
+ */
+ public void dispatchActivityCreated() {
+ mHost.mFragmentManager.dispatchActivityCreated();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the start state.
+ * <p>Call when Fragments should be started.
+ *
+ * @see Fragment#onStart()
+ */
+ public void dispatchStart() {
+ mHost.mFragmentManager.dispatchStart();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the resume state.
+ * <p>Call when Fragments should be resumed.
+ *
+ * @see Fragment#onResume()
+ */
+ public void dispatchResume() {
+ mHost.mFragmentManager.dispatchResume();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the pause state.
+ * <p>Call when Fragments should be paused.
+ *
+ * @see Fragment#onPause()
+ */
+ public void dispatchPause() {
+ mHost.mFragmentManager.dispatchPause();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the stop state.
+ * <p>Call when Fragments should be stopped.
+ *
+ * @see Fragment#onStop()
+ */
+ public void dispatchStop() {
+ mHost.mFragmentManager.dispatchStop();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the destroy view state.
+ * <p>Call when the Fragment's views should be destroyed.
+ *
+ * @see Fragment#onDestroyView()
+ */
+ public void dispatchDestroyView() {
+ mHost.mFragmentManager.dispatchDestroyView();
+ }
+
+ /**
+ * Moves all Fragments managed by the controller's FragmentManager
+ * into the destroy state.
+ * <p>Call when Fragments should be destroyed.
+ *
+ * @see Fragment#onDestroy()
+ */
+ public void dispatchDestroy() {
+ mHost.mFragmentManager.dispatchDestroy();
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager know the multi-window mode of
+ * the activity changed.
+ * <p>Call when the multi-window mode of the activity changed.
+ *
+ * @see Fragment#onMultiWindowModeChanged
+ * @deprecated use {@link #dispatchMultiWindowModeChanged(boolean, Configuration)}
+ */
+ @Deprecated
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ mHost.mFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager know the multi-window mode of
+ * the activity changed.
+ * <p>Call when the multi-window mode of the activity changed.
+ *
+ * @see Fragment#onMultiWindowModeChanged
+ */
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode,
+ Configuration newConfig) {
+ mHost.mFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager know the picture-in-picture
+ * mode of the activity changed.
+ * <p>Call when the picture-in-picture mode of the activity changed.
+ *
+ * @see Fragment#onPictureInPictureModeChanged
+ * @deprecated use {@link #dispatchPictureInPictureModeChanged(boolean, Configuration)}
+ */
+ @Deprecated
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ mHost.mFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager know the picture-in-picture
+ * mode of the activity changed.
+ * <p>Call when the picture-in-picture mode of the activity changed.
+ *
+ * @see Fragment#onPictureInPictureModeChanged
+ */
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ mHost.mFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode,
+ newConfig);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know a configuration change occurred.
+ * <p>Call when there is a configuration change.
+ *
+ * @see Fragment#onConfigurationChanged(Configuration)
+ */
+ public void dispatchConfigurationChanged(Configuration newConfig) {
+ mHost.mFragmentManager.dispatchConfigurationChanged(newConfig);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know the device is in a low memory condition.
+ * <p>Call when the device is low on memory and Fragment's should trim
+ * their memory usage.
+ *
+ * @see Fragment#onLowMemory()
+ */
+ public void dispatchLowMemory() {
+ mHost.mFragmentManager.dispatchLowMemory();
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know they should trim their memory usage.
+ * <p>Call when the Fragment can release allocated memory [such as if
+ * the Fragment is in the background].
+ *
+ * @see Fragment#onTrimMemory(int)
+ */
+ public void dispatchTrimMemory(int level) {
+ mHost.mFragmentManager.dispatchTrimMemory(level);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know they should create an options menu.
+ * <p>Call when the Fragment should create an options menu.
+ *
+ * @return {@code true} if the options menu contains items to display
+ * @see Fragment#onCreateOptionsMenu(Menu, MenuInflater)
+ */
+ public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ return mHost.mFragmentManager.dispatchCreateOptionsMenu(menu, inflater);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know they should prepare their options menu for display.
+ * <p>Call immediately before displaying the Fragment's options menu.
+ *
+ * @return {@code true} if the options menu contains items to display
+ * @see Fragment#onPrepareOptionsMenu(Menu)
+ */
+ public boolean dispatchPrepareOptionsMenu(Menu menu) {
+ return mHost.mFragmentManager.dispatchPrepareOptionsMenu(menu);
+ }
+
+ /**
+ * Sends an option item selection event to the Fragments managed by the
+ * controller's FragmentManager. Once the event has been consumed,
+ * no additional handling will be performed.
+ * <p>Call immediately after an options menu item has been selected
+ *
+ * @return {@code true} if the options menu selection event was consumed
+ * @see Fragment#onOptionsItemSelected(MenuItem)
+ */
+ public boolean dispatchOptionsItemSelected(MenuItem item) {
+ return mHost.mFragmentManager.dispatchOptionsItemSelected(item);
+ }
+
+ /**
+ * Sends a context item selection event to the Fragments managed by the
+ * controller's FragmentManager. Once the event has been consumed,
+ * no additional handling will be performed.
+ * <p>Call immediately after an options menu item has been selected
+ *
+ * @return {@code true} if the context menu selection event was consumed
+ * @see Fragment#onContextItemSelected(MenuItem)
+ */
+ public boolean dispatchContextItemSelected(MenuItem item) {
+ return mHost.mFragmentManager.dispatchContextItemSelected(item);
+ }
+
+ /**
+ * Lets all Fragments managed by the controller's FragmentManager
+ * know their options menu has closed.
+ * <p>Call immediately after closing the Fragment's options menu.
+ *
+ * @see Fragment#onOptionsMenuClosed(Menu)
+ */
+ public void dispatchOptionsMenuClosed(Menu menu) {
+ mHost.mFragmentManager.dispatchOptionsMenuClosed(menu);
+ }
+
+ /**
+ * Execute any pending actions for the Fragments managed by the
+ * controller's FragmentManager.
+ * <p>Call when queued actions can be performed [eg when the
+ * Fragment moves into a start or resume state].
+ * @return {@code true} if queued actions were performed
+ */
+ public boolean execPendingActions() {
+ return mHost.mFragmentManager.execPendingActions();
+ }
+
+ /**
+ * Starts the loaders.
+ */
+ public void doLoaderStart() {
+ mHost.doLoaderStart();
+ }
+
+ /**
+ * Stops the loaders, optionally retaining their state. This is useful for keeping the
+ * loader state across configuration changes.
+ *
+ * @param retain When {@code true}, the loaders aren't stopped, but, their instances
+ * are retained in a started state
+ */
+ public void doLoaderStop(boolean retain) {
+ mHost.doLoaderStop(retain);
+ }
+
+ /**
+ * Destroys the loaders and, if their state is not being retained, removes them.
+ */
+ public void doLoaderDestroy() {
+ mHost.doLoaderDestroy();
+ }
+
+ /**
+ * Lets the loaders know the host is ready to receive notifications.
+ */
+ public void reportLoaderStart() {
+ mHost.reportLoaderStart();
+ }
+
+ /**
+ * Returns a list of LoaderManagers that have opted to retain their instance across
+ * configuration changes.
+ */
+ public ArrayMap<String, LoaderManager> retainLoaderNonConfig() {
+ return mHost.retainLoaderNonConfig();
+ }
+
+ /**
+ * Restores the saved state for all LoaderManagers. The given LoaderManager list are
+ * LoaderManager instances retained across configuration changes.
+ *
+ * @see #retainLoaderNonConfig()
+ */
+ public void restoreLoaderNonConfig(ArrayMap<String, LoaderManager> loaderManagers) {
+ mHost.restoreLoaderNonConfig(loaderManagers);
+ }
+
+ /**
+ * Dumps the current state of the loaders.
+ */
+ public void dumpLoaders(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ mHost.dumpLoaders(prefix, fd, writer, args);
+ }
+}
diff --git a/android/app/FragmentHostCallback.java b/android/app/FragmentHostCallback.java
new file mode 100644
index 00000000..5ef23e63
--- /dev/null
+++ b/android/app/FragmentHostCallback.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Integration points with the Fragment host.
+ * <p>
+ * Fragments may be hosted by any object; such as an {@link Activity}. In order to
+ * host fragments, implement {@link FragmentHostCallback}, overriding the methods
+ * applicable to the host.
+ */
+public abstract class FragmentHostCallback<E> extends FragmentContainer {
+ private final Activity mActivity;
+ final Context mContext;
+ private final Handler mHandler;
+ final int mWindowAnimations;
+ final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();
+ /** The loader managers for individual fragments [i.e. Fragment#getLoaderManager()] */
+ private ArrayMap<String, LoaderManager> mAllLoaderManagers;
+ /** Whether or not fragment loaders should retain their state */
+ private boolean mRetainLoaders;
+ /** The loader manger for the fragment host [i.e. Activity#getLoaderManager()] */
+ private LoaderManagerImpl mLoaderManager;
+ private boolean mCheckedForLoaderManager;
+ /** Whether or not the fragment host loader manager was started */
+ private boolean mLoadersStarted;
+
+ public FragmentHostCallback(Context context, Handler handler, int windowAnimations) {
+ this((context instanceof Activity) ? (Activity)context : null, context,
+ chooseHandler(context, handler), windowAnimations);
+ }
+
+ FragmentHostCallback(Activity activity) {
+ this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/);
+ }
+
+ FragmentHostCallback(Activity activity, Context context, Handler handler,
+ int windowAnimations) {
+ mActivity = activity;
+ mContext = context;
+ mHandler = handler;
+ mWindowAnimations = windowAnimations;
+ }
+
+ /**
+ * Used internally in {@link #FragmentHostCallback(Context, Handler, int)} to choose
+ * the Activity's handler or the provided handler.
+ */
+ private static Handler chooseHandler(Context context, Handler handler) {
+ if (handler == null && context instanceof Activity) {
+ Activity activity = (Activity) context;
+ return activity.mHandler;
+ } else {
+ return handler;
+ }
+ }
+
+ /**
+ * Print internal state into the given stream.
+ *
+ * @param prefix Desired prefix to prepend at each line of output.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be closed
+ * for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+
+ /**
+ * Return {@code true} if the fragment's state needs to be saved.
+ */
+ public boolean onShouldSaveFragmentState(Fragment fragment) {
+ return true;
+ }
+
+ /**
+ * Return a {@link LayoutInflater}.
+ * See {@link Activity#getLayoutInflater()}.
+ */
+ public LayoutInflater onGetLayoutInflater() {
+ return (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Return {@code true} if the FragmentManager's LayoutInflaterFactory should be used.
+ */
+ public boolean onUseFragmentManagerInflaterFactory() {
+ return false;
+ }
+
+ /**
+ * Return the object that's currently hosting the fragment. If a {@link Fragment}
+ * is hosted by a {@link Activity}, the object returned here should be the same
+ * object returned from {@link Fragment#getActivity()}.
+ */
+ @Nullable
+ public abstract E onGetHost();
+
+ /**
+ * Invalidates the activity's options menu.
+ * See {@link Activity#invalidateOptionsMenu()}
+ */
+ public void onInvalidateOptionsMenu() {
+ }
+
+ /**
+ * Starts a new {@link Activity} from the given fragment.
+ * See {@link Activity#startActivityForResult(Intent, int)}.
+ */
+ public void onStartActivityFromFragment(Fragment fragment, Intent intent, int requestCode,
+ Bundle options) {
+ if (requestCode != -1) {
+ throw new IllegalStateException(
+ "Starting activity with a requestCode requires a FragmentActivity host");
+ }
+ mContext.startActivity(intent);
+ }
+
+ /**
+ * @hide
+ * Starts a new {@link Activity} from the given fragment.
+ * See {@link Activity#startActivityForResult(Intent, int)}.
+ */
+ public void onStartActivityAsUserFromFragment(Fragment fragment, Intent intent, int requestCode,
+ Bundle options, UserHandle userHandle) {
+ if (requestCode != -1) {
+ throw new IllegalStateException(
+ "Starting activity with a requestCode requires a FragmentActivity host");
+ }
+ mContext.startActivityAsUser(intent, userHandle);
+ }
+
+ /**
+ * Starts a new {@link IntentSender} from the given fragment.
+ * See {@link Activity#startIntentSender(IntentSender, Intent, int, int, int, Bundle)}.
+ */
+ public void onStartIntentSenderFromFragment(Fragment fragment, IntentSender intent,
+ int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,
+ int extraFlags, Bundle options) throws IntentSender.SendIntentException {
+ if (requestCode != -1) {
+ throw new IllegalStateException(
+ "Starting intent sender with a requestCode requires a FragmentActivity host");
+ }
+ mContext.startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags,
+ options);
+ }
+
+ /**
+ * Requests permissions from the given fragment.
+ * See {@link Activity#requestPermissions(String[], int)}
+ */
+ public void onRequestPermissionsFromFragment(@NonNull Fragment fragment,
+ @NonNull String[] permissions, int requestCode) {
+ }
+
+ /**
+ * Return {@code true} if there are window animations.
+ */
+ public boolean onHasWindowAnimations() {
+ return true;
+ }
+
+ /**
+ * Return the window animations.
+ */
+ public int onGetWindowAnimations() {
+ return mWindowAnimations;
+ }
+
+ /**
+ * Called when a {@link Fragment} is being attached to this host, immediately
+ * after the call to its {@link Fragment#onAttach(Context)} method and before
+ * {@link Fragment#onCreate(Bundle)}.
+ */
+ public void onAttachFragment(Fragment fragment) {
+ }
+
+ @Nullable
+ @Override
+ public <T extends View> T onFindViewById(int id) {
+ return null;
+ }
+
+ @Override
+ public boolean onHasView() {
+ return true;
+ }
+
+ boolean getRetainLoaders() {
+ return mRetainLoaders;
+ }
+
+ Activity getActivity() {
+ return mActivity;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ Handler getHandler() {
+ return mHandler;
+ }
+
+ FragmentManagerImpl getFragmentManagerImpl() {
+ return mFragmentManager;
+ }
+
+ LoaderManagerImpl getLoaderManagerImpl() {
+ if (mLoaderManager != null) {
+ return mLoaderManager;
+ }
+ mCheckedForLoaderManager = true;
+ mLoaderManager = getLoaderManager("(root)", mLoadersStarted, true /*create*/);
+ return mLoaderManager;
+ }
+
+ void inactivateFragment(String who) {
+ //Log.v(TAG, "invalidateSupportFragment: who=" + who);
+ if (mAllLoaderManagers != null) {
+ LoaderManagerImpl lm = (LoaderManagerImpl) mAllLoaderManagers.get(who);
+ if (lm != null && !lm.mRetaining) {
+ lm.doDestroy();
+ mAllLoaderManagers.remove(who);
+ }
+ }
+ }
+
+ void doLoaderStart() {
+ if (mLoadersStarted) {
+ return;
+ }
+ mLoadersStarted = true;
+
+ if (mLoaderManager != null) {
+ mLoaderManager.doStart();
+ } else if (!mCheckedForLoaderManager) {
+ mLoaderManager = getLoaderManager("(root)", mLoadersStarted, false);
+ }
+ mCheckedForLoaderManager = true;
+ }
+
+ void doLoaderStop(boolean retain) {
+ mRetainLoaders = retain;
+
+ if (mLoaderManager == null) {
+ return;
+ }
+
+ if (!mLoadersStarted) {
+ return;
+ }
+ mLoadersStarted = false;
+
+ if (retain) {
+ mLoaderManager.doRetain();
+ } else {
+ mLoaderManager.doStop();
+ }
+ }
+
+ void doLoaderRetain() {
+ if (mLoaderManager == null) {
+ return;
+ }
+ mLoaderManager.doRetain();
+ }
+
+ void doLoaderDestroy() {
+ if (mLoaderManager == null) {
+ return;
+ }
+ mLoaderManager.doDestroy();
+ }
+
+ void reportLoaderStart() {
+ if (mAllLoaderManagers != null) {
+ final int N = mAllLoaderManagers.size();
+ LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
+ for (int i=N-1; i>=0; i--) {
+ loaders[i] = (LoaderManagerImpl) mAllLoaderManagers.valueAt(i);
+ }
+ for (int i=0; i<N; i++) {
+ LoaderManagerImpl lm = loaders[i];
+ lm.finishRetain();
+ lm.doReportStart();
+ }
+ }
+ }
+
+ LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) {
+ if (mAllLoaderManagers == null) {
+ mAllLoaderManagers = new ArrayMap<String, LoaderManager>();
+ }
+ LoaderManagerImpl lm = (LoaderManagerImpl) mAllLoaderManagers.get(who);
+ if (lm == null && create) {
+ lm = new LoaderManagerImpl(who, this, started);
+ mAllLoaderManagers.put(who, lm);
+ } else if (started && lm != null && !lm.mStarted){
+ lm.doStart();
+ }
+ return lm;
+ }
+
+ ArrayMap<String, LoaderManager> retainLoaderNonConfig() {
+ boolean retainLoaders = false;
+ if (mAllLoaderManagers != null) {
+ // Restart any loader managers that were already stopped so that they
+ // will be ready to retain
+ final int N = mAllLoaderManagers.size();
+ LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
+ for (int i=N-1; i>=0; i--) {
+ loaders[i] = (LoaderManagerImpl) mAllLoaderManagers.valueAt(i);
+ }
+ final boolean doRetainLoaders = getRetainLoaders();
+ for (int i=0; i<N; i++) {
+ LoaderManagerImpl lm = loaders[i];
+ if (!lm.mRetaining && doRetainLoaders) {
+ if (!lm.mStarted) {
+ lm.doStart();
+ }
+ lm.doRetain();
+ }
+ if (lm.mRetaining) {
+ retainLoaders = true;
+ } else {
+ lm.doDestroy();
+ mAllLoaderManagers.remove(lm.mWho);
+ }
+ }
+ }
+
+ if (retainLoaders) {
+ return mAllLoaderManagers;
+ }
+ return null;
+ }
+
+ void restoreLoaderNonConfig(ArrayMap<String, LoaderManager> loaderManagers) {
+ if (loaderManagers != null) {
+ for (int i = 0, N = loaderManagers.size(); i < N; i++) {
+ ((LoaderManagerImpl) loaderManagers.valueAt(i)).updateHostController(this);
+ }
+ }
+ mAllLoaderManagers = loaderManagers;
+ }
+
+ void dumpLoaders(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mLoadersStarted=");
+ writer.println(mLoadersStarted);
+ if (mLoaderManager != null) {
+ writer.print(prefix); writer.print("Loader Manager ");
+ writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager)));
+ writer.println(":");
+ mLoaderManager.dump(prefix + " ", fd, writer, args);
+ }
+ }
+}
diff --git a/android/app/FragmentManager.java b/android/app/FragmentManager.java
new file mode 100644
index 00000000..ba5ea218
--- /dev/null
+++ b/android/app/FragmentManager.java
@@ -0,0 +1,3711 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.LogWriter;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SuperNotCalledException;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Interface for interacting with {@link Fragment} objects inside of an
+ * {@link Activity}
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using fragments, read the
+ * <a href="{@docRoot}guide/components/fragments.html">Fragments</a> developer guide.</p>
+ * </div>
+ *
+ * While the FragmentManager API was introduced in
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, a version of the API
+ * at is also available for use on older platforms through
+ * {@link android.support.v4.app.FragmentActivity}. See the blog post
+ * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html">
+ * Fragments For All</a> for more details.
+ */
+public abstract class FragmentManager {
+ /**
+ * Representation of an entry on the fragment back stack, as created
+ * with {@link FragmentTransaction#addToBackStack(String)
+ * FragmentTransaction.addToBackStack()}. Entries can later be
+ * retrieved with {@link FragmentManager#getBackStackEntryAt(int)
+ * FragmentManager.getBackStackEntryAt()}.
+ *
+ * <p>Note that you should never hold on to a BackStackEntry object;
+ * the identifier as returned by {@link #getId} is the only thing that
+ * will be persisted across activity instances.
+ */
+ public interface BackStackEntry {
+ /**
+ * Return the unique identifier for the entry. This is the only
+ * representation of the entry that will persist across activity
+ * instances.
+ */
+ public int getId();
+
+ /**
+ * Get the name that was supplied to
+ * {@link FragmentTransaction#addToBackStack(String)
+ * FragmentTransaction.addToBackStack(String)} when creating this entry.
+ */
+ public String getName();
+
+ /**
+ * Return the full bread crumb title resource identifier for the entry,
+ * or 0 if it does not have one.
+ */
+ public int getBreadCrumbTitleRes();
+
+ /**
+ * Return the short bread crumb title resource identifier for the entry,
+ * or 0 if it does not have one.
+ */
+ public int getBreadCrumbShortTitleRes();
+
+ /**
+ * Return the full bread crumb title for the entry, or null if it
+ * does not have one.
+ */
+ public CharSequence getBreadCrumbTitle();
+
+ /**
+ * Return the short bread crumb title for the entry, or null if it
+ * does not have one.
+ */
+ public CharSequence getBreadCrumbShortTitle();
+ }
+
+ /**
+ * Interface to watch for changes to the back stack.
+ */
+ public interface OnBackStackChangedListener {
+ /**
+ * Called whenever the contents of the back stack change.
+ */
+ public void onBackStackChanged();
+ }
+
+ /**
+ * Start a series of edit operations on the Fragments associated with
+ * this FragmentManager.
+ *
+ * <p>Note: A fragment transaction can only be created/committed prior
+ * to an activity saving its state. If you try to commit a transaction
+ * after {@link Activity#onSaveInstanceState Activity.onSaveInstanceState()}
+ * (and prior to a following {@link Activity#onStart Activity.onStart}
+ * or {@link Activity#onResume Activity.onResume()}, you will get an error.
+ * This is because the framework takes care of saving your current fragments
+ * in the state, and if changes are made after the state is saved then they
+ * will be lost.</p>
+ */
+ public abstract FragmentTransaction beginTransaction();
+
+ /** @hide -- remove once prebuilts are in. */
+ @Deprecated
+ public FragmentTransaction openTransaction() {
+ return beginTransaction();
+ }
+
+ /**
+ * After a {@link FragmentTransaction} is committed with
+ * {@link FragmentTransaction#commit FragmentTransaction.commit()}, it
+ * is scheduled to be executed asynchronously on the process's main thread.
+ * If you want to immediately executing any such pending operations, you
+ * can call this function (only from the main thread) to do so. Note that
+ * all callbacks and other related behavior will be done from within this
+ * call, so be careful about where this is called from.
+ * <p>
+ * This also forces the start of any postponed Transactions where
+ * {@link Fragment#postponeEnterTransition()} has been called.
+ *
+ * @return Returns true if there were any pending transactions to be
+ * executed.
+ */
+ public abstract boolean executePendingTransactions();
+
+ /**
+ * Finds a fragment that was identified by the given id either when inflated
+ * from XML or as the container ID when added in a transaction. This first
+ * searches through fragments that are currently added to the manager's
+ * activity; if no such fragment is found, then all fragments currently
+ * on the back stack associated with this ID are searched.
+ * @return The fragment if found or null otherwise.
+ */
+ public abstract Fragment findFragmentById(int id);
+
+ /**
+ * Finds a fragment that was identified by the given tag either when inflated
+ * from XML or as supplied when added in a transaction. This first
+ * searches through fragments that are currently added to the manager's
+ * activity; if no such fragment is found, then all fragments currently
+ * on the back stack are searched.
+ * @return The fragment if found or null otherwise.
+ */
+ public abstract Fragment findFragmentByTag(String tag);
+
+ /**
+ * Flag for {@link #popBackStack(String, int)}
+ * and {@link #popBackStack(int, int)}: If set, and the name or ID of
+ * a back stack entry has been supplied, then all matching entries will
+ * be consumed until one that doesn't match is found or the bottom of
+ * the stack is reached. Otherwise, all entries up to but not including that entry
+ * will be removed.
+ */
+ public static final int POP_BACK_STACK_INCLUSIVE = 1<<0;
+
+ /**
+ * Pop the top state off the back stack. This function is asynchronous -- it
+ * enqueues the request to pop, but the action will not be performed until the
+ * application returns to its event loop.
+ */
+ public abstract void popBackStack();
+
+ /**
+ * Like {@link #popBackStack()}, but performs the operation immediately
+ * inside of the call. This is like calling {@link #executePendingTransactions()}
+ * afterwards without forcing the start of postponed Transactions.
+ * @return Returns true if there was something popped, else false.
+ */
+ public abstract boolean popBackStackImmediate();
+
+ /**
+ * Pop the last fragment transition from the manager's fragment
+ * back stack. If there is nothing to pop, false is returned.
+ * This function is asynchronous -- it enqueues the
+ * request to pop, but the action will not be performed until the application
+ * returns to its event loop.
+ *
+ * @param name If non-null, this is the name of a previous back state
+ * to look for; if found, all states up to that state will be popped. The
+ * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
+ * the named state itself is popped. If null, only the top state is popped.
+ * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.
+ */
+ public abstract void popBackStack(String name, int flags);
+
+ /**
+ * Like {@link #popBackStack(String, int)}, but performs the operation immediately
+ * inside of the call. This is like calling {@link #executePendingTransactions()}
+ * afterwards without forcing the start of postponed Transactions.
+ * @return Returns true if there was something popped, else false.
+ */
+ public abstract boolean popBackStackImmediate(String name, int flags);
+
+ /**
+ * Pop all back stack states up to the one with the given identifier.
+ * This function is asynchronous -- it enqueues the
+ * request to pop, but the action will not be performed until the application
+ * returns to its event loop.
+ *
+ * @param id Identifier of the stated to be popped. If no identifier exists,
+ * false is returned.
+ * The identifier is the number returned by
+ * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The
+ * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
+ * the named state itself is popped.
+ * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.
+ */
+ public abstract void popBackStack(int id, int flags);
+
+ /**
+ * Like {@link #popBackStack(int, int)}, but performs the operation immediately
+ * inside of the call. This is like calling {@link #executePendingTransactions()}
+ * afterwards without forcing the start of postponed Transactions.
+ * @return Returns true if there was something popped, else false.
+ */
+ public abstract boolean popBackStackImmediate(int id, int flags);
+
+ /**
+ * Return the number of entries currently in the back stack.
+ */
+ public abstract int getBackStackEntryCount();
+
+ /**
+ * Return the BackStackEntry at index <var>index</var> in the back stack;
+ * where the item on the bottom of the stack has index 0.
+ */
+ public abstract BackStackEntry getBackStackEntryAt(int index);
+
+ /**
+ * Add a new listener for changes to the fragment back stack.
+ */
+ public abstract void addOnBackStackChangedListener(OnBackStackChangedListener listener);
+
+ /**
+ * Remove a listener that was previously added with
+ * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}.
+ */
+ public abstract void removeOnBackStackChangedListener(OnBackStackChangedListener listener);
+
+ /**
+ * Put a reference to a fragment in a Bundle. This Bundle can be
+ * persisted as saved state, and when later restoring
+ * {@link #getFragment(Bundle, String)} will return the current
+ * instance of the same fragment.
+ *
+ * @param bundle The bundle in which to put the fragment reference.
+ * @param key The name of the entry in the bundle.
+ * @param fragment The Fragment whose reference is to be stored.
+ */
+ public abstract void putFragment(Bundle bundle, String key, Fragment fragment);
+
+ /**
+ * Retrieve the current Fragment instance for a reference previously
+ * placed with {@link #putFragment(Bundle, String, Fragment)}.
+ *
+ * @param bundle The bundle from which to retrieve the fragment reference.
+ * @param key The name of the entry in the bundle.
+ * @return Returns the current Fragment instance that is associated with
+ * the given reference.
+ */
+ public abstract Fragment getFragment(Bundle bundle, String key);
+
+ /**
+ * Get a list of all fragments that are currently added to the FragmentManager.
+ * This may include those that are hidden as well as those that are shown.
+ * This will not include any fragments only in the back stack, or fragments that
+ * are detached or removed.
+ * <p>
+ * The order of the fragments in the list is the order in which they were
+ * added or attached.
+ *
+ * @return A list of all fragments that are added to the FragmentManager.
+ */
+ public abstract List<Fragment> getFragments();
+
+ /**
+ * Save the current instance state of the given Fragment. This can be
+ * used later when creating a new instance of the Fragment and adding
+ * it to the fragment manager, to have it create itself to match the
+ * current state returned here. Note that there are limits on how
+ * this can be used:
+ *
+ * <ul>
+ * <li>The Fragment must currently be attached to the FragmentManager.
+ * <li>A new Fragment created using this saved state must be the same class
+ * type as the Fragment it was created from.
+ * <li>The saved state can not contain dependencies on other fragments --
+ * that is it can't use {@link #putFragment(Bundle, String, Fragment)} to
+ * store a fragment reference because that reference may not be valid when
+ * this saved state is later used. Likewise the Fragment's target and
+ * result code are not included in this state.
+ * </ul>
+ *
+ * @param f The Fragment whose state is to be saved.
+ * @return The generated state. This will be null if there was no
+ * interesting state created by the fragment.
+ */
+ public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f);
+
+ /**
+ * Returns true if the final {@link Activity#onDestroy() Activity.onDestroy()}
+ * call has been made on the FragmentManager's Activity, so this instance is now dead.
+ */
+ public abstract boolean isDestroyed();
+
+ /**
+ * Registers a {@link FragmentLifecycleCallbacks} to listen to fragment lifecycle events
+ * happening in this FragmentManager. All registered callbacks will be automatically
+ * unregistered when this FragmentManager is destroyed.
+ *
+ * @param cb Callbacks to register
+ * @param recursive true to automatically register this callback for all child FragmentManagers
+ */
+ public abstract void registerFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb,
+ boolean recursive);
+
+ /**
+ * Unregisters a previously registered {@link FragmentLifecycleCallbacks}. If the callback
+ * was not previously registered this call has no effect. All registered callbacks will be
+ * automatically unregistered when this FragmentManager is destroyed.
+ *
+ * @param cb Callbacks to unregister
+ */
+ public abstract void unregisterFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb);
+
+ /**
+ * Return the currently active primary navigation fragment for this FragmentManager.
+ *
+ * <p>The primary navigation fragment's
+ * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first
+ * to process delegated navigation actions such as {@link #popBackStack()} if no ID
+ * or transaction name is provided to pop to.</p>
+ *
+ * @return the fragment designated as the primary navigation fragment
+ */
+ public abstract Fragment getPrimaryNavigationFragment();
+
+ /**
+ * Print the FragmentManager's state into the given stream.
+ *
+ * @param prefix Text to print at the front of each line.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer A PrintWriter to which the dump is to be set.
+ * @param args Additional arguments to the dump request.
+ */
+ public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args);
+
+ /**
+ * Control whether the framework's internal fragment manager debugging
+ * logs are turned on. If enabled, you will see output in logcat as
+ * the framework performs fragment operations.
+ */
+ public static void enableDebugLogging(boolean enabled) {
+ FragmentManagerImpl.DEBUG = enabled;
+ }
+
+ /**
+ * Invalidate the attached activity's options menu as necessary.
+ * This may end up being deferred until we move to the resumed state.
+ */
+ public void invalidateOptionsMenu() { }
+
+ /**
+ * Returns {@code true} if the FragmentManager's state has already been saved
+ * by its host. Any operations that would change saved state should not be performed
+ * if this method returns true. For example, any popBackStack() method, such as
+ * {@link #popBackStackImmediate()} or any FragmentTransaction using
+ * {@link FragmentTransaction#commit()} instead of
+ * {@link FragmentTransaction#commitAllowingStateLoss()} will change
+ * the state and will result in an error.
+ *
+ * @return true if this FragmentManager's state has already been saved by its host
+ */
+ public abstract boolean isStateSaved();
+
+ /**
+ * Callback interface for listening to fragment state changes that happen
+ * within a given FragmentManager.
+ */
+ public abstract static class FragmentLifecycleCallbacks {
+ /**
+ * Called right before the fragment's {@link Fragment#onAttach(Context)} method is called.
+ * This is a good time to inject any required dependencies for the fragment before any of
+ * the fragment's lifecycle methods are invoked.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param context Context that the Fragment is being attached to
+ */
+ public void onFragmentPreAttached(FragmentManager fm, Fragment f, Context context) {}
+
+ /**
+ * Called after the fragment has been attached to its host. Its host will have had
+ * <code>onAttachFragment</code> called before this call happens.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param context Context that the Fragment was attached to
+ */
+ public void onFragmentAttached(FragmentManager fm, Fragment f, Context context) {}
+
+ /**
+ * Called right before the fragment's {@link Fragment#onCreate(Bundle)} method is called.
+ * This is a good time to inject any required dependencies or perform other configuration
+ * for the fragment.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param savedInstanceState Saved instance bundle from a previous instance
+ */
+ public void onFragmentPreCreated(FragmentManager fm, Fragment f,
+ Bundle savedInstanceState) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onCreate(Bundle)}. This will only happen once for any given
+ * fragment instance, though the fragment may be attached and detached multiple times.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param savedInstanceState Saved instance bundle from a previous instance
+ */
+ public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onActivityCreated(Bundle)}. This will only happen once for any given
+ * fragment instance, though the fragment may be attached and detached multiple times.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param savedInstanceState Saved instance bundle from a previous instance
+ */
+ public void onFragmentActivityCreated(FragmentManager fm, Fragment f,
+ Bundle savedInstanceState) {}
+
+ /**
+ * Called after the fragment has returned a non-null view from the FragmentManager's
+ * request to {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment that created and owns the view
+ * @param v View returned by the fragment
+ * @param savedInstanceState Saved instance bundle from a previous instance
+ */
+ public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
+ Bundle savedInstanceState) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onStart()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentStarted(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onResume()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentResumed(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onPause()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentPaused(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onStop()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentStopped(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onSaveInstanceState(Bundle)}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ * @param outState Saved state bundle for the fragment
+ */
+ public void onFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onDestroyView()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onDestroy()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}
+
+ /**
+ * Called after the fragment has returned from the FragmentManager's call to
+ * {@link Fragment#onDetach()}.
+ *
+ * @param fm Host FragmentManager
+ * @param f Fragment changing state
+ */
+ public void onFragmentDetached(FragmentManager fm, Fragment f) {}
+ }
+}
+
+final class FragmentManagerState implements Parcelable {
+ FragmentState[] mActive;
+ int[] mAdded;
+ BackStackState[] mBackStack;
+ int mPrimaryNavActiveIndex = -1;
+ int mNextFragmentIndex;
+
+ public FragmentManagerState() {
+ }
+
+ public FragmentManagerState(Parcel in) {
+ mActive = in.createTypedArray(FragmentState.CREATOR);
+ mAdded = in.createIntArray();
+ mBackStack = in.createTypedArray(BackStackState.CREATOR);
+ mPrimaryNavActiveIndex = in.readInt();
+ mNextFragmentIndex = in.readInt();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedArray(mActive, flags);
+ dest.writeIntArray(mAdded);
+ dest.writeTypedArray(mBackStack, flags);
+ dest.writeInt(mPrimaryNavActiveIndex);
+ dest.writeInt(mNextFragmentIndex);
+ }
+
+ public static final Parcelable.Creator<FragmentManagerState> CREATOR
+ = new Parcelable.Creator<FragmentManagerState>() {
+ public FragmentManagerState createFromParcel(Parcel in) {
+ return new FragmentManagerState(in);
+ }
+
+ public FragmentManagerState[] newArray(int size) {
+ return new FragmentManagerState[size];
+ }
+ };
+}
+
+/**
+ * Container for fragments associated with an activity.
+ */
+final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 {
+ static boolean DEBUG = false;
+ static final String TAG = "FragmentManager";
+
+ static final String TARGET_REQUEST_CODE_STATE_TAG = "android:target_req_state";
+ static final String TARGET_STATE_TAG = "android:target_state";
+ static final String VIEW_STATE_TAG = "android:view_state";
+ static final String USER_VISIBLE_HINT_TAG = "android:user_visible_hint";
+
+ static class AnimateOnHWLayerIfNeededListener implements Animator.AnimatorListener {
+ private boolean mShouldRunOnHWLayer = false;
+ private View mView;
+ public AnimateOnHWLayerIfNeededListener(final View v) {
+ if (v == null) {
+ return;
+ }
+ mView = v;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mShouldRunOnHWLayer = shouldRunOnHWLayer(mView, animation);
+ if (mShouldRunOnHWLayer) {
+ mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mShouldRunOnHWLayer) {
+ mView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ mView = null;
+ animation.removeListener(this);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ }
+
+ ArrayList<OpGenerator> mPendingActions;
+ boolean mExecutingActions;
+
+ int mNextFragmentIndex = 0;
+ SparseArray<Fragment> mActive;
+ final ArrayList<Fragment> mAdded = new ArrayList<>();
+ ArrayList<BackStackRecord> mBackStack;
+ ArrayList<Fragment> mCreatedMenus;
+
+ // Must be accessed while locked.
+ ArrayList<BackStackRecord> mBackStackIndices;
+ ArrayList<Integer> mAvailBackStackIndices;
+
+ ArrayList<OnBackStackChangedListener> mBackStackChangeListeners;
+ final CopyOnWriteArrayList<Pair<FragmentLifecycleCallbacks, Boolean>>
+ mLifecycleCallbacks = new CopyOnWriteArrayList<>();
+
+ int mCurState = Fragment.INITIALIZING;
+ FragmentHostCallback<?> mHost;
+ FragmentContainer mContainer;
+ Fragment mParent;
+ Fragment mPrimaryNav;
+
+ boolean mNeedMenuInvalidate;
+ boolean mStateSaved;
+ boolean mDestroyed;
+ String mNoTransactionsBecause;
+ boolean mHavePendingDeferredStart;
+
+ // Temporary vars for removing redundant operations in BackStackRecords:
+ ArrayList<BackStackRecord> mTmpRecords;
+ ArrayList<Boolean> mTmpIsPop;
+ ArrayList<Fragment> mTmpAddedFragments;
+
+ // Temporary vars for state save and restore.
+ Bundle mStateBundle = null;
+ SparseArray<Parcelable> mStateArray = null;
+
+ // Postponed transactions.
+ ArrayList<StartEnterTransitionListener> mPostponedTransactions;
+
+ // Prior to O, we allowed executing transactions during fragment manager state changes.
+ // This is dangerous, but we want to keep from breaking old applications.
+ boolean mAllowOldReentrantBehavior;
+
+ // Saved FragmentManagerNonConfig during saveAllState() and cleared in noteStateNotSaved()
+ FragmentManagerNonConfig mSavedNonConfig;
+
+ Runnable mExecCommit = new Runnable() {
+ @Override
+ public void run() {
+ execPendingActions();
+ }
+ };
+
+ private void throwException(RuntimeException ex) {
+ Log.e(TAG, ex.getMessage());
+ LogWriter logw = new LogWriter(Log.ERROR, TAG);
+ PrintWriter pw = new FastPrintWriter(logw, false, 1024);
+ if (mHost != null) {
+ Log.e(TAG, "Activity state:");
+ try {
+ mHost.onDump(" ", null, pw, new String[] { });
+ } catch (Exception e) {
+ pw.flush();
+ Log.e(TAG, "Failed dumping state", e);
+ }
+ } else {
+ Log.e(TAG, "Fragment manager state:");
+ try {
+ dump(" ", null, pw, new String[] { });
+ } catch (Exception e) {
+ pw.flush();
+ Log.e(TAG, "Failed dumping state", e);
+ }
+ }
+ pw.flush();
+ throw ex;
+ }
+
+ static boolean modifiesAlpha(Animator anim) {
+ if (anim == null) {
+ return false;
+ }
+ if (anim instanceof ValueAnimator) {
+ ValueAnimator valueAnim = (ValueAnimator) anim;
+ PropertyValuesHolder[] values = valueAnim.getValues();
+ for (int i = 0; i < values.length; i++) {
+ if (("alpha").equals(values[i].getPropertyName())) {
+ return true;
+ }
+ }
+ } else if (anim instanceof AnimatorSet) {
+ List<Animator> animList = ((AnimatorSet) anim).getChildAnimations();
+ for (int i = 0; i < animList.size(); i++) {
+ if (modifiesAlpha(animList.get(i))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static boolean shouldRunOnHWLayer(View v, Animator anim) {
+ if (v == null || anim == null) {
+ return false;
+ }
+ return v.getLayerType() == View.LAYER_TYPE_NONE
+ && v.hasOverlappingRendering()
+ && modifiesAlpha(anim);
+ }
+
+ /**
+ * Sets the to be animated view on hardware layer during the animation.
+ */
+ private void setHWLayerAnimListenerIfAlpha(final View v, Animator anim) {
+ if (v == null || anim == null) {
+ return;
+ }
+ if (shouldRunOnHWLayer(v, anim)) {
+ anim.addListener(new AnimateOnHWLayerIfNeededListener(v));
+ }
+ }
+
+ @Override
+ public FragmentTransaction beginTransaction() {
+ return new BackStackRecord(this);
+ }
+
+ @Override
+ public boolean executePendingTransactions() {
+ boolean updates = execPendingActions();
+ forcePostponedTransactions();
+ return updates;
+ }
+
+ @Override
+ public void popBackStack() {
+ enqueueAction(new PopBackStackState(null, -1, 0), false);
+ }
+
+ @Override
+ public boolean popBackStackImmediate() {
+ checkStateLoss();
+ return popBackStackImmediate(null, -1, 0);
+ }
+
+ @Override
+ public void popBackStack(String name, int flags) {
+ enqueueAction(new PopBackStackState(name, -1, flags), false);
+ }
+
+ @Override
+ public boolean popBackStackImmediate(String name, int flags) {
+ checkStateLoss();
+ return popBackStackImmediate(name, -1, flags);
+ }
+
+ @Override
+ public void popBackStack(int id, int flags) {
+ if (id < 0) {
+ throw new IllegalArgumentException("Bad id: " + id);
+ }
+ enqueueAction(new PopBackStackState(null, id, flags), false);
+ }
+
+ @Override
+ public boolean popBackStackImmediate(int id, int flags) {
+ checkStateLoss();
+ if (id < 0) {
+ throw new IllegalArgumentException("Bad id: " + id);
+ }
+ return popBackStackImmediate(null, id, flags);
+ }
+
+ /**
+ * Used by all public popBackStackImmediate methods, this executes pending transactions and
+ * returns true if the pop action did anything, regardless of what other pending
+ * transactions did.
+ *
+ * @return true if the pop operation did anything or false otherwise.
+ */
+ private boolean popBackStackImmediate(String name, int id, int flags) {
+ execPendingActions();
+ ensureExecReady(true);
+
+ if (mPrimaryNav != null // We have a primary nav fragment
+ && id < 0 // No valid id (since they're local)
+ && name == null) { // no name to pop to (since they're local)
+ final FragmentManager childManager = mPrimaryNav.mChildFragmentManager;
+ if (childManager != null && childManager.popBackStackImmediate()) {
+ // We did something, just not to this specific FragmentManager. Return true.
+ return true;
+ }
+ }
+
+ boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags);
+ if (executePop) {
+ mExecutingActions = true;
+ try {
+ removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
+ } finally {
+ cleanupExec();
+ }
+ }
+
+ doPendingDeferredStart();
+ burpActive();
+ return executePop;
+ }
+
+ @Override
+ public int getBackStackEntryCount() {
+ return mBackStack != null ? mBackStack.size() : 0;
+ }
+
+ @Override
+ public BackStackEntry getBackStackEntryAt(int index) {
+ return mBackStack.get(index);
+ }
+
+ @Override
+ public void addOnBackStackChangedListener(OnBackStackChangedListener listener) {
+ if (mBackStackChangeListeners == null) {
+ mBackStackChangeListeners = new ArrayList<OnBackStackChangedListener>();
+ }
+ mBackStackChangeListeners.add(listener);
+ }
+
+ @Override
+ public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) {
+ if (mBackStackChangeListeners != null) {
+ mBackStackChangeListeners.remove(listener);
+ }
+ }
+
+ @Override
+ public void putFragment(Bundle bundle, String key, Fragment fragment) {
+ if (fragment.mIndex < 0) {
+ throwException(new IllegalStateException("Fragment " + fragment
+ + " is not currently in the FragmentManager"));
+ }
+ bundle.putInt(key, fragment.mIndex);
+ }
+
+ @Override
+ public Fragment getFragment(Bundle bundle, String key) {
+ int index = bundle.getInt(key, -1);
+ if (index == -1) {
+ return null;
+ }
+ Fragment f = mActive.get(index);
+ if (f == null) {
+ throwException(new IllegalStateException("Fragment no longer exists for key "
+ + key + ": index " + index));
+ }
+ return f;
+ }
+
+ @Override
+ public List<Fragment> getFragments() {
+ if (mAdded.isEmpty()) {
+ return Collections.EMPTY_LIST;
+ }
+ synchronized (mAdded) {
+ return (List<Fragment>) mAdded.clone();
+ }
+ }
+
+ @Override
+ public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
+ if (fragment.mIndex < 0) {
+ throwException(new IllegalStateException("Fragment " + fragment
+ + " is not currently in the FragmentManager"));
+ }
+ if (fragment.mState > Fragment.INITIALIZING) {
+ Bundle result = saveFragmentBasicState(fragment);
+ return result != null ? new Fragment.SavedState(result) : null;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("FragmentManager{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" in ");
+ if (mParent != null) {
+ DebugUtils.buildShortClassTag(mParent, sb);
+ } else {
+ DebugUtils.buildShortClassTag(mHost, sb);
+ }
+ sb.append("}}");
+ return sb.toString();
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ String innerPrefix = prefix + " ";
+
+ int N;
+ if (mActive != null) {
+ N = mActive.size();
+ if (N > 0) {
+ writer.print(prefix); writer.print("Active Fragments in ");
+ writer.print(Integer.toHexString(System.identityHashCode(this)));
+ writer.println(":");
+ for (int i=0; i<N; i++) {
+ Fragment f = mActive.valueAt(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": "); writer.println(f);
+ if (f != null) {
+ f.dump(innerPrefix, fd, writer, args);
+ }
+ }
+ }
+ }
+
+ N = mAdded.size();
+ if (N > 0) {
+ writer.print(prefix);
+ writer.println("Added Fragments:");
+ for (int i = 0; i < N; i++) {
+ Fragment f = mAdded.get(i);
+ writer.print(prefix);
+ writer.print(" #");
+ writer.print(i);
+ writer.print(": ");
+ writer.println(f.toString());
+ }
+ }
+
+ if (mCreatedMenus != null) {
+ N = mCreatedMenus.size();
+ if (N > 0) {
+ writer.print(prefix); writer.println("Fragments Created Menus:");
+ for (int i=0; i<N; i++) {
+ Fragment f = mCreatedMenus.get(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": "); writer.println(f.toString());
+ }
+ }
+ }
+
+ if (mBackStack != null) {
+ N = mBackStack.size();
+ if (N > 0) {
+ writer.print(prefix); writer.println("Back Stack:");
+ for (int i=0; i<N; i++) {
+ BackStackRecord bs = mBackStack.get(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": "); writer.println(bs.toString());
+ bs.dump(innerPrefix, fd, writer, args);
+ }
+ }
+ }
+
+ synchronized (this) {
+ if (mBackStackIndices != null) {
+ N = mBackStackIndices.size();
+ if (N > 0) {
+ writer.print(prefix); writer.println("Back Stack Indices:");
+ for (int i=0; i<N; i++) {
+ BackStackRecord bs = mBackStackIndices.get(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": "); writer.println(bs);
+ }
+ }
+ }
+
+ if (mAvailBackStackIndices != null && mAvailBackStackIndices.size() > 0) {
+ writer.print(prefix); writer.print("mAvailBackStackIndices: ");
+ writer.println(Arrays.toString(mAvailBackStackIndices.toArray()));
+ }
+ }
+
+ if (mPendingActions != null) {
+ N = mPendingActions.size();
+ if (N > 0) {
+ writer.print(prefix); writer.println("Pending Actions:");
+ for (int i=0; i<N; i++) {
+ OpGenerator r = mPendingActions.get(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": "); writer.println(r);
+ }
+ }
+ }
+
+ writer.print(prefix); writer.println("FragmentManager misc state:");
+ writer.print(prefix); writer.print(" mHost="); writer.println(mHost);
+ writer.print(prefix); writer.print(" mContainer="); writer.println(mContainer);
+ if (mParent != null) {
+ writer.print(prefix); writer.print(" mParent="); writer.println(mParent);
+ }
+ writer.print(prefix); writer.print(" mCurState="); writer.print(mCurState);
+ writer.print(" mStateSaved="); writer.print(mStateSaved);
+ writer.print(" mDestroyed="); writer.println(mDestroyed);
+ if (mNeedMenuInvalidate) {
+ writer.print(prefix); writer.print(" mNeedMenuInvalidate=");
+ writer.println(mNeedMenuInvalidate);
+ }
+ if (mNoTransactionsBecause != null) {
+ writer.print(prefix); writer.print(" mNoTransactionsBecause=");
+ writer.println(mNoTransactionsBecause);
+ }
+ }
+
+ Animator loadAnimator(Fragment fragment, int transit, boolean enter,
+ int transitionStyle) {
+ Animator animObj = fragment.onCreateAnimator(transit, enter, fragment.getNextAnim());
+ if (animObj != null) {
+ return animObj;
+ }
+
+ if (fragment.getNextAnim() != 0) {
+ Animator anim = AnimatorInflater.loadAnimator(mHost.getContext(),
+ fragment.getNextAnim());
+ if (anim != null) {
+ return anim;
+ }
+ }
+
+ if (transit == 0) {
+ return null;
+ }
+
+ int styleIndex = transitToStyleIndex(transit, enter);
+ if (styleIndex < 0) {
+ return null;
+ }
+
+ if (transitionStyle == 0 && mHost.onHasWindowAnimations()) {
+ transitionStyle = mHost.onGetWindowAnimations();
+ }
+ if (transitionStyle == 0) {
+ return null;
+ }
+
+ TypedArray attrs = mHost.getContext().obtainStyledAttributes(transitionStyle,
+ com.android.internal.R.styleable.FragmentAnimation);
+ int anim = attrs.getResourceId(styleIndex, 0);
+ attrs.recycle();
+
+ if (anim == 0) {
+ return null;
+ }
+
+ return AnimatorInflater.loadAnimator(mHost.getContext(), anim);
+ }
+
+ public void performPendingDeferredStart(Fragment f) {
+ if (f.mDeferStart) {
+ if (mExecutingActions) {
+ // Wait until we're done executing our pending transactions
+ mHavePendingDeferredStart = true;
+ return;
+ }
+ f.mDeferStart = false;
+ moveToState(f, mCurState, 0, 0, false);
+ }
+ }
+
+ boolean isStateAtLeast(int state) {
+ return mCurState >= state;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ void moveToState(Fragment f, int newState, int transit, int transitionStyle,
+ boolean keepActive) {
+ if (DEBUG && false) Log.v(TAG, "moveToState: " + f
+ + " oldState=" + f.mState + " newState=" + newState
+ + " mRemoving=" + f.mRemoving + " Callers=" + Debug.getCallers(5));
+
+ // Fragments that are not currently added will sit in the onCreate() state.
+ if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
+ newState = Fragment.CREATED;
+ }
+ if (f.mRemoving && newState > f.mState) {
+ if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
+ // Allow the fragment to be created so that it can be saved later.
+ newState = Fragment.CREATED;
+ } else {
+ // While removing a fragment, we can't change it to a higher state.
+ newState = f.mState;
+ }
+ }
+ // Defer start if requested; don't allow it to move to STARTED or higher
+ // if it's not already started.
+ if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.STOPPED) {
+ newState = Fragment.STOPPED;
+ }
+ if (f.mState <= newState) {
+ // For fragments that are created from a layout, when restoring from
+ // state we don't want to allow them to be created until they are
+ // being reloaded from the layout.
+ if (f.mFromLayout && !f.mInLayout) {
+ return;
+ }
+ if (f.getAnimatingAway() != null) {
+ // The fragment is currently being animated... but! Now we
+ // want to move our state back up. Give up on waiting for the
+ // animation, move to whatever the final state should be once
+ // the animation is done, and then we can proceed from there.
+ f.setAnimatingAway(null);
+ moveToState(f, f.getStateAfterAnimating(), 0, 0, true);
+ }
+ switch (f.mState) {
+ case Fragment.INITIALIZING:
+ if (newState > Fragment.INITIALIZING) {
+ if (DEBUG) Log.v(TAG, "moveto CREATED: " + f);
+ if (f.mSavedFragmentState != null) {
+ f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(
+ FragmentManagerImpl.VIEW_STATE_TAG);
+ f.mTarget = getFragment(f.mSavedFragmentState,
+ FragmentManagerImpl.TARGET_STATE_TAG);
+ if (f.mTarget != null) {
+ f.mTargetRequestCode = f.mSavedFragmentState.getInt(
+ FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);
+ }
+ f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
+ FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
+ if (!f.mUserVisibleHint) {
+ f.mDeferStart = true;
+ if (newState > Fragment.STOPPED) {
+ newState = Fragment.STOPPED;
+ }
+ }
+ }
+
+ f.mHost = mHost;
+ f.mParentFragment = mParent;
+ f.mFragmentManager = mParent != null
+ ? mParent.mChildFragmentManager : mHost.getFragmentManagerImpl();
+
+ // If we have a target fragment, push it along to at least CREATED
+ // so that this one can rely on it as an initialized dependency.
+ if (f.mTarget != null) {
+ if (mActive.get(f.mTarget.mIndex) != f.mTarget) {
+ throw new IllegalStateException("Fragment " + f
+ + " declared target fragment " + f.mTarget
+ + " that does not belong to this FragmentManager!");
+ }
+ if (f.mTarget.mState < Fragment.CREATED) {
+ moveToState(f.mTarget, Fragment.CREATED, 0, 0, true);
+ }
+ }
+
+ dispatchOnFragmentPreAttached(f, mHost.getContext(), false);
+ f.mCalled = false;
+ f.onAttach(mHost.getContext());
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onAttach()");
+ }
+ if (f.mParentFragment == null) {
+ mHost.onAttachFragment(f);
+ } else {
+ f.mParentFragment.onAttachFragment(f);
+ }
+ dispatchOnFragmentAttached(f, mHost.getContext(), false);
+
+ if (!f.mIsCreated) {
+ dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false);
+ f.performCreate(f.mSavedFragmentState);
+ dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);
+ } else {
+ f.restoreChildFragmentState(f.mSavedFragmentState, true);
+ f.mState = Fragment.CREATED;
+ }
+ f.mRetaining = false;
+ }
+ // fall through
+ case Fragment.CREATED:
+ // This is outside the if statement below on purpose; we want this to run
+ // even if we do a moveToState from CREATED => *, CREATED => CREATED, and
+ // * => CREATED as part of the case fallthrough above.
+ ensureInflatedFragmentView(f);
+
+ if (newState > Fragment.CREATED) {
+ if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
+ if (!f.mFromLayout) {
+ ViewGroup container = null;
+ if (f.mContainerId != 0) {
+ if (f.mContainerId == View.NO_ID) {
+ throwException(new IllegalArgumentException(
+ "Cannot create fragment "
+ + f
+ + " for a container view with no id"));
+ }
+ container = mContainer.onFindViewById(f.mContainerId);
+ if (container == null && !f.mRestored) {
+ String resName;
+ try {
+ resName = f.getResources().getResourceName(f.mContainerId);
+ } catch (NotFoundException e) {
+ resName = "unknown";
+ }
+ throwException(new IllegalArgumentException(
+ "No view found for id 0x"
+ + Integer.toHexString(f.mContainerId) + " ("
+ + resName
+ + ") for fragment " + f));
+ }
+ }
+ f.mContainer = container;
+ f.mView = f.performCreateView(f.performGetLayoutInflater(
+ f.mSavedFragmentState), container, f.mSavedFragmentState);
+ if (f.mView != null) {
+ f.mView.setSaveFromParentEnabled(false);
+ if (container != null) {
+ container.addView(f.mView);
+ }
+ if (f.mHidden) {
+ f.mView.setVisibility(View.GONE);
+ }
+ f.onViewCreated(f.mView, f.mSavedFragmentState);
+ dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
+ false);
+ // Only animate the view if it is visible. This is done after
+ // dispatchOnFragmentViewCreated in case visibility is changed
+ f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
+ && f.mContainer != null;
+ }
+ }
+
+ f.performActivityCreated(f.mSavedFragmentState);
+ dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
+ if (f.mView != null) {
+ f.restoreViewState(f.mSavedFragmentState);
+ }
+ f.mSavedFragmentState = null;
+ }
+ // fall through
+ case Fragment.ACTIVITY_CREATED:
+ if (newState > Fragment.ACTIVITY_CREATED) {
+ f.mState = Fragment.STOPPED;
+ }
+ // fall through
+ case Fragment.STOPPED:
+ if (newState > Fragment.STOPPED) {
+ if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
+ f.performStart();
+ dispatchOnFragmentStarted(f, false);
+ }
+ // fall through
+ case Fragment.STARTED:
+ if (newState > Fragment.STARTED) {
+ if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
+ f.performResume();
+ dispatchOnFragmentResumed(f, false);
+ // Get rid of this in case we saved it and never needed it.
+ f.mSavedFragmentState = null;
+ f.mSavedViewState = null;
+ }
+ }
+ } else if (f.mState > newState) {
+ switch (f.mState) {
+ case Fragment.RESUMED:
+ if (newState < Fragment.RESUMED) {
+ if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f);
+ f.performPause();
+ dispatchOnFragmentPaused(f, false);
+ }
+ // fall through
+ case Fragment.STARTED:
+ if (newState < Fragment.STARTED) {
+ if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f);
+ f.performStop();
+ dispatchOnFragmentStopped(f, false);
+ }
+ // fall through
+ case Fragment.STOPPED:
+ case Fragment.ACTIVITY_CREATED:
+ if (newState < Fragment.ACTIVITY_CREATED) {
+ if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);
+ if (f.mView != null) {
+ // Need to save the current view state if not
+ // done already.
+ if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
+ saveFragmentViewState(f);
+ }
+ }
+ f.performDestroyView();
+ dispatchOnFragmentViewDestroyed(f, false);
+ if (f.mView != null && f.mContainer != null) {
+ if (getTargetSdk() >= Build.VERSION_CODES.O) {
+ // Stop any current animations:
+ f.mView.clearAnimation();
+ f.mContainer.endViewTransition(f.mView);
+ }
+ Animator anim = null;
+ if (mCurState > Fragment.INITIALIZING && !mDestroyed
+ && f.mView.getVisibility() == View.VISIBLE
+ && f.mView.getTransitionAlpha() > 0) {
+ anim = loadAnimator(f, transit, false,
+ transitionStyle);
+ }
+ f.mView.setTransitionAlpha(1f);
+ if (anim != null) {
+ final ViewGroup container = f.mContainer;
+ final View view = f.mView;
+ final Fragment fragment = f;
+ container.startViewTransition(view);
+ f.setAnimatingAway(anim);
+ f.setStateAfterAnimating(newState);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ container.endViewTransition(view);
+ if (fragment.getAnimatingAway() != null) {
+ fragment.setAnimatingAway(null);
+ moveToState(fragment, fragment.getStateAfterAnimating(),
+ 0, 0, false);
+ }
+ }
+ });
+ anim.setTarget(f.mView);
+ setHWLayerAnimListenerIfAlpha(f.mView, anim);
+ anim.start();
+
+ }
+ f.mContainer.removeView(f.mView);
+ }
+ f.mContainer = null;
+ f.mView = null;
+ f.mInLayout = false;
+ }
+ // fall through
+ case Fragment.CREATED:
+ if (newState < Fragment.CREATED) {
+ if (mDestroyed) {
+ if (f.getAnimatingAway() != null) {
+ // The fragment's containing activity is
+ // being destroyed, but this fragment is
+ // currently animating away. Stop the
+ // animation right now -- it is not needed,
+ // and we can't wait any more on destroying
+ // the fragment.
+ Animator anim = f.getAnimatingAway();
+ f.setAnimatingAway(null);
+ anim.cancel();
+ }
+ }
+ if (f.getAnimatingAway() != null) {
+ // We are waiting for the fragment's view to finish
+ // animating away. Just make a note of the state
+ // the fragment now should move to once the animation
+ // is done.
+ f.setStateAfterAnimating(newState);
+ newState = Fragment.CREATED;
+ } else {
+ if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f);
+ if (!f.mRetaining) {
+ f.performDestroy();
+ dispatchOnFragmentDestroyed(f, false);
+ } else {
+ f.mState = Fragment.INITIALIZING;
+ }
+
+ f.performDetach();
+ dispatchOnFragmentDetached(f, false);
+ if (!keepActive) {
+ if (!f.mRetaining) {
+ makeInactive(f);
+ } else {
+ f.mHost = null;
+ f.mParentFragment = null;
+ f.mFragmentManager = null;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (f.mState != newState) {
+ Log.w(TAG, "moveToState: Fragment state for " + f + " not updated inline; "
+ + "expected state " + newState + " found " + f.mState);
+ f.mState = newState;
+ }
+ }
+
+ void moveToState(Fragment f) {
+ moveToState(f, mCurState, 0, 0, false);
+ }
+
+ void ensureInflatedFragmentView(Fragment f) {
+ if (f.mFromLayout && !f.mPerformedCreateView) {
+ f.mView = f.performCreateView(f.performGetLayoutInflater(
+ f.mSavedFragmentState), null, f.mSavedFragmentState);
+ if (f.mView != null) {
+ f.mView.setSaveFromParentEnabled(false);
+ if (f.mHidden) f.mView.setVisibility(View.GONE);
+ f.onViewCreated(f.mView, f.mSavedFragmentState);
+ dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);
+ }
+ }
+ }
+
+ /**
+ * Fragments that have been shown or hidden don't have their visibility changed or
+ * animations run during the {@link #showFragment(Fragment)} or {@link #hideFragment(Fragment)}
+ * calls. After fragments are brought to their final state in
+ * {@link #moveFragmentToExpectedState(Fragment)} the fragments that have been shown or
+ * hidden must have their visibility changed and their animations started here.
+ *
+ * @param fragment The fragment with mHiddenChanged = true that should change its View's
+ * visibility and start the show or hide animation.
+ */
+ void completeShowHideFragment(final Fragment fragment) {
+ if (fragment.mView != null) {
+ Animator anim = loadAnimator(fragment, fragment.getNextTransition(), !fragment.mHidden,
+ fragment.getNextTransitionStyle());
+ if (anim != null) {
+ anim.setTarget(fragment.mView);
+ if (fragment.mHidden) {
+ if (fragment.isHideReplaced()) {
+ fragment.setHideReplaced(false);
+ } else {
+ final ViewGroup container = fragment.mContainer;
+ final View animatingView = fragment.mView;
+ if (container != null) {
+ container.startViewTransition(animatingView);
+ }
+ // Delay the actual hide operation until the animation finishes, otherwise
+ // the fragment will just immediately disappear
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (container != null) {
+ container.endViewTransition(animatingView);
+ }
+ animation.removeListener(this);
+ animatingView.setVisibility(View.GONE);
+ }
+ });
+ }
+ } else {
+ fragment.mView.setVisibility(View.VISIBLE);
+ }
+ setHWLayerAnimListenerIfAlpha(fragment.mView, anim);
+ anim.start();
+ } else {
+ final int visibility = fragment.mHidden && !fragment.isHideReplaced()
+ ? View.GONE
+ : View.VISIBLE;
+ fragment.mView.setVisibility(visibility);
+ if (fragment.isHideReplaced()) {
+ fragment.setHideReplaced(false);
+ }
+ }
+ }
+ if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.mHiddenChanged = false;
+ fragment.onHiddenChanged(fragment.mHidden);
+ }
+
+ /**
+ * Moves a fragment to its expected final state or the fragment manager's state, depending
+ * on whether the fragment manager's state is raised properly.
+ *
+ * @param f The fragment to change.
+ */
+ void moveFragmentToExpectedState(final Fragment f) {
+ if (f == null) {
+ return;
+ }
+ int nextState = mCurState;
+ if (f.mRemoving) {
+ if (f.isInBackStack()) {
+ nextState = Math.min(nextState, Fragment.CREATED);
+ } else {
+ nextState = Math.min(nextState, Fragment.INITIALIZING);
+ }
+ }
+
+ moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false);
+
+ if (f.mView != null) {
+ // Move the view if it is out of order
+ Fragment underFragment = findFragmentUnder(f);
+ if (underFragment != null) {
+ final View underView = underFragment.mView;
+ // make sure this fragment is in the right order.
+ final ViewGroup container = f.mContainer;
+ int underIndex = container.indexOfChild(underView);
+ int viewIndex = container.indexOfChild(f.mView);
+ if (viewIndex < underIndex) {
+ container.removeViewAt(viewIndex);
+ container.addView(f.mView, underIndex);
+ }
+ }
+ if (f.mIsNewlyAdded && f.mContainer != null) {
+ // Make it visible and run the animations
+ f.mView.setTransitionAlpha(1f);
+ f.mIsNewlyAdded = false;
+ // run animations:
+ Animator anim = loadAnimator(f, f.getNextTransition(), true, f.getNextTransitionStyle());
+ if (anim != null) {
+ anim.setTarget(f.mView);
+ setHWLayerAnimListenerIfAlpha(f.mView, anim);
+ anim.start();
+ }
+ }
+ }
+ if (f.mHiddenChanged) {
+ completeShowHideFragment(f);
+ }
+ }
+
+ /**
+ * Changes the state of the fragment manager to {@code newState}. If the fragment manager
+ * changes state or {@code always} is {@code true}, any fragments within it have their
+ * states updated as well.
+ *
+ * @param newState The new state for the fragment manager
+ * @param always If {@code true}, all fragments update their state, even
+ * if {@code newState} matches the current fragment manager's state.
+ */
+ void moveToState(int newState, boolean always) {
+ if (mHost == null && newState != Fragment.INITIALIZING) {
+ throw new IllegalStateException("No activity");
+ }
+
+ if (!always && mCurState == newState) {
+ return;
+ }
+
+ mCurState = newState;
+
+ if (mActive != null) {
+ boolean loadersRunning = false;
+
+ // Must add them in the proper order. mActive fragments may be out of order
+ final int numAdded = mAdded.size();
+ for (int i = 0; i < numAdded; i++) {
+ Fragment f = mAdded.get(i);
+ moveFragmentToExpectedState(f);
+ if (f.mLoaderManager != null) {
+ loadersRunning |= f.mLoaderManager.hasRunningLoaders();
+ }
+ }
+
+ // Now iterate through all active fragments. These will include those that are removed
+ // and detached.
+ final int numActive = mActive.size();
+ for (int i = 0; i < numActive; i++) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) {
+ moveFragmentToExpectedState(f);
+ if (f.mLoaderManager != null) {
+ loadersRunning |= f.mLoaderManager.hasRunningLoaders();
+ }
+ }
+ }
+
+ if (!loadersRunning) {
+ startPendingDeferredFragments();
+ }
+
+ if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) {
+ mHost.onInvalidateOptionsMenu();
+ mNeedMenuInvalidate = false;
+ }
+ }
+ }
+
+ void startPendingDeferredFragments() {
+ if (mActive == null) return;
+
+ for (int i=0; i<mActive.size(); i++) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null) {
+ performPendingDeferredStart(f);
+ }
+ }
+ }
+
+ void makeActive(Fragment f) {
+ if (f.mIndex >= 0) {
+ return;
+ }
+
+ f.setIndex(mNextFragmentIndex++, mParent);
+ if (mActive == null) {
+ mActive = new SparseArray<>();
+ }
+ mActive.put(f.mIndex, f);
+ if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
+ }
+
+ void makeInactive(Fragment f) {
+ if (f.mIndex < 0) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "Freeing fragment index " + f);
+ // Don't remove yet. That happens in burpActive(). This prevents
+ // concurrent modification while iterating over mActive
+ mActive.put(f.mIndex, null);
+ mHost.inactivateFragment(f.mWho);
+ f.initState();
+ }
+
+ public void addFragment(Fragment fragment, boolean moveToStateNow) {
+ if (DEBUG) Log.v(TAG, "add: " + fragment);
+ makeActive(fragment);
+ if (!fragment.mDetached) {
+ if (mAdded.contains(fragment)) {
+ throw new IllegalStateException("Fragment already added: " + fragment);
+ }
+ synchronized (mAdded) {
+ mAdded.add(fragment);
+ }
+ fragment.mAdded = true;
+ fragment.mRemoving = false;
+ if (fragment.mView == null) {
+ fragment.mHiddenChanged = false;
+ }
+ if (fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ if (moveToStateNow) {
+ moveToState(fragment);
+ }
+ }
+ }
+
+ public void removeFragment(Fragment fragment) {
+ if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);
+ final boolean inactive = !fragment.isInBackStack();
+ if (!fragment.mDetached || inactive) {
+ if (false) {
+ // Would be nice to catch a bad remove here, but we need
+ // time to test this to make sure we aren't crashes cases
+ // where it is not a problem.
+ if (!mAdded.contains(fragment)) {
+ throw new IllegalStateException("Fragment not added: " + fragment);
+ }
+ }
+ synchronized (mAdded) {
+ mAdded.remove(fragment);
+ }
+ if (fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.mAdded = false;
+ fragment.mRemoving = true;
+ }
+ }
+
+ /**
+ * Marks a fragment as hidden to be later animated in with
+ * {@link #completeShowHideFragment(Fragment)}.
+ *
+ * @param fragment The fragment to be shown.
+ */
+ public void hideFragment(Fragment fragment) {
+ if (DEBUG) Log.v(TAG, "hide: " + fragment);
+ if (!fragment.mHidden) {
+ fragment.mHidden = true;
+ // Toggle hidden changed so that if a fragment goes through show/hide/show
+ // it doesn't go through the animation.
+ fragment.mHiddenChanged = !fragment.mHiddenChanged;
+ }
+ }
+
+ /**
+ * Marks a fragment as shown to be later animated in with
+ * {@link #completeShowHideFragment(Fragment)}.
+ *
+ * @param fragment The fragment to be shown.
+ */
+ public void showFragment(Fragment fragment) {
+ if (DEBUG) Log.v(TAG, "show: " + fragment);
+ if (fragment.mHidden) {
+ fragment.mHidden = false;
+ // Toggle hidden changed so that if a fragment goes through show/hide/show
+ // it doesn't go through the animation.
+ fragment.mHiddenChanged = !fragment.mHiddenChanged;
+ }
+ }
+
+ public void detachFragment(Fragment fragment) {
+ if (DEBUG) Log.v(TAG, "detach: " + fragment);
+ if (!fragment.mDetached) {
+ fragment.mDetached = true;
+ if (fragment.mAdded) {
+ // We are not already in back stack, so need to remove the fragment.
+ if (DEBUG) Log.v(TAG, "remove from detach: " + fragment);
+ synchronized (mAdded) {
+ mAdded.remove(fragment);
+ }
+ if (fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.mAdded = false;
+ }
+ }
+ }
+
+ public void attachFragment(Fragment fragment) {
+ if (DEBUG) Log.v(TAG, "attach: " + fragment);
+ if (fragment.mDetached) {
+ fragment.mDetached = false;
+ if (!fragment.mAdded) {
+ if (mAdded.contains(fragment)) {
+ throw new IllegalStateException("Fragment already added: " + fragment);
+ }
+ if (DEBUG) Log.v(TAG, "add from attach: " + fragment);
+ synchronized (mAdded) {
+ mAdded.add(fragment);
+ }
+ fragment.mAdded = true;
+ if (fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ }
+ }
+ }
+
+ public Fragment findFragmentById(int id) {
+ // First look through added fragments.
+ for (int i = mAdded.size() - 1; i >= 0; i--) {
+ Fragment f = mAdded.get(i);
+ if (f != null && f.mFragmentId == id) {
+ return f;
+ }
+ }
+ if (mActive != null) {
+ // Now for any known fragment.
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null && f.mFragmentId == id) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Fragment findFragmentByTag(String tag) {
+ if (tag != null) {
+ // First look through added fragments.
+ for (int i=mAdded.size()-1; i>=0; i--) {
+ Fragment f = mAdded.get(i);
+ if (f != null && tag.equals(f.mTag)) {
+ return f;
+ }
+ }
+ }
+ if (mActive != null && tag != null) {
+ // Now for any known fragment.
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null && tag.equals(f.mTag)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Fragment findFragmentByWho(String who) {
+ if (mActive != null && who != null) {
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null && (f=f.findFragmentByWho(who)) != null) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void checkStateLoss() {
+ if (mStateSaved) {
+ throw new IllegalStateException(
+ "Can not perform this action after onSaveInstanceState");
+ }
+ if (mNoTransactionsBecause != null) {
+ throw new IllegalStateException(
+ "Can not perform this action inside of " + mNoTransactionsBecause);
+ }
+ }
+
+ @Override
+ public boolean isStateSaved() {
+ return mStateSaved;
+ }
+
+ /**
+ * Adds an action to the queue of pending actions.
+ *
+ * @param action the action to add
+ * @param allowStateLoss whether to allow loss of state information
+ * @throws IllegalStateException if the activity has been destroyed
+ */
+ public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
+ if (!allowStateLoss) {
+ checkStateLoss();
+ }
+ synchronized (this) {
+ if (mDestroyed || mHost == null) {
+ if (allowStateLoss) {
+ // This FragmentManager isn't attached, so drop the entire transaction.
+ return;
+ }
+ throw new IllegalStateException("Activity has been destroyed");
+ }
+ if (mPendingActions == null) {
+ mPendingActions = new ArrayList<>();
+ }
+ mPendingActions.add(action);
+ scheduleCommit();
+ }
+ }
+
+ /**
+ * Schedules the execution when one hasn't been scheduled already. This should happen
+ * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when
+ * a postponed transaction has been started with
+ * {@link Fragment#startPostponedEnterTransition()}
+ */
+ private void scheduleCommit() {
+ synchronized (this) {
+ boolean postponeReady =
+ mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
+ boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
+ if (postponeReady || pendingReady) {
+ mHost.getHandler().removeCallbacks(mExecCommit);
+ mHost.getHandler().post(mExecCommit);
+ }
+ }
+ }
+
+ public int allocBackStackIndex(BackStackRecord bse) {
+ synchronized (this) {
+ if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
+ if (mBackStackIndices == null) {
+ mBackStackIndices = new ArrayList<BackStackRecord>();
+ }
+ int index = mBackStackIndices.size();
+ if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse);
+ mBackStackIndices.add(bse);
+ return index;
+
+ } else {
+ int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
+ if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse);
+ mBackStackIndices.set(index, bse);
+ return index;
+ }
+ }
+ }
+
+ public void setBackStackIndex(int index, BackStackRecord bse) {
+ synchronized (this) {
+ if (mBackStackIndices == null) {
+ mBackStackIndices = new ArrayList<BackStackRecord>();
+ }
+ int N = mBackStackIndices.size();
+ if (index < N) {
+ if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse);
+ mBackStackIndices.set(index, bse);
+ } else {
+ while (N < index) {
+ mBackStackIndices.add(null);
+ if (mAvailBackStackIndices == null) {
+ mAvailBackStackIndices = new ArrayList<Integer>();
+ }
+ if (DEBUG) Log.v(TAG, "Adding available back stack index " + N);
+ mAvailBackStackIndices.add(N);
+ N++;
+ }
+ if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse);
+ mBackStackIndices.add(bse);
+ }
+ }
+ }
+
+ public void freeBackStackIndex(int index) {
+ synchronized (this) {
+ mBackStackIndices.set(index, null);
+ if (mAvailBackStackIndices == null) {
+ mAvailBackStackIndices = new ArrayList<Integer>();
+ }
+ if (DEBUG) Log.v(TAG, "Freeing back stack index " + index);
+ mAvailBackStackIndices.add(index);
+ }
+ }
+
+ /**
+ * Broken out from exec*, this prepares for gathering and executing operations.
+ *
+ * @param allowStateLoss true if state loss should be ignored or false if it should be
+ * checked.
+ */
+ private void ensureExecReady(boolean allowStateLoss) {
+ if (mExecutingActions) {
+ throw new IllegalStateException("FragmentManager is already executing transactions");
+ }
+
+ if (Looper.myLooper() != mHost.getHandler().getLooper()) {
+ throw new IllegalStateException("Must be called from main thread of fragment host");
+ }
+
+ if (!allowStateLoss) {
+ checkStateLoss();
+ }
+
+ if (mTmpRecords == null) {
+ mTmpRecords = new ArrayList<>();
+ mTmpIsPop = new ArrayList<>();
+ }
+ mExecutingActions = true;
+ try {
+ executePostponedTransaction(null, null);
+ } finally {
+ mExecutingActions = false;
+ }
+ }
+
+ public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
+ if (allowStateLoss && (mHost == null || mDestroyed)) {
+ // This FragmentManager isn't attached, so drop the entire transaction.
+ return;
+ }
+ ensureExecReady(allowStateLoss);
+ if (action.generateOps(mTmpRecords, mTmpIsPop)) {
+ mExecutingActions = true;
+ try {
+ removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
+ } finally {
+ cleanupExec();
+ }
+ }
+
+ doPendingDeferredStart();
+ burpActive();
+ }
+
+ /**
+ * Broken out of exec*, this cleans up the mExecutingActions and the temporary structures
+ * used in executing operations.
+ */
+ private void cleanupExec() {
+ mExecutingActions = false;
+ mTmpIsPop.clear();
+ mTmpRecords.clear();
+ }
+
+ /**
+ * Only call from main thread!
+ */
+ public boolean execPendingActions() {
+ ensureExecReady(true);
+
+ boolean didSomething = false;
+ while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
+ mExecutingActions = true;
+ try {
+ removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
+ } finally {
+ cleanupExec();
+ }
+ didSomething = true;
+ }
+
+ doPendingDeferredStart();
+ burpActive();
+
+ return didSomething;
+ }
+
+ /**
+ * Complete the execution of transactions that have previously been postponed, but are
+ * now ready.
+ */
+ private void executePostponedTransaction(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop) {
+ int numPostponed = mPostponedTransactions == null ? 0 : mPostponedTransactions.size();
+ for (int i = 0; i < numPostponed; i++) {
+ StartEnterTransitionListener listener = mPostponedTransactions.get(i);
+ if (records != null && !listener.mIsBack) {
+ int index = records.indexOf(listener.mRecord);
+ if (index != -1 && isRecordPop.get(index)) {
+ listener.cancelTransaction();
+ continue;
+ }
+ }
+ if (listener.isReady() || (records != null &&
+ listener.mRecord.interactsWith(records, 0, records.size()))) {
+ mPostponedTransactions.remove(i);
+ i--;
+ numPostponed--;
+ int index;
+ if (records != null && !listener.mIsBack &&
+ (index = records.indexOf(listener.mRecord)) != -1 &&
+ isRecordPop.get(index)) {
+ // This is popping a postponed transaction
+ listener.cancelTransaction();
+ } else {
+ listener.completeTransaction();
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove redundant BackStackRecord operations and executes them. This method merges operations
+ * of proximate records that allow reordering. See
+ * {@link FragmentTransaction#setReorderingAllowed(boolean)}.
+ * <p>
+ * For example, a transaction that adds to the back stack and then another that pops that
+ * back stack record will be optimized to remove the unnecessary operation.
+ * <p>
+ * Likewise, two transactions committed that are executed at the same time will be optimized
+ * to remove the redundant operations as well as two pop operations executed together.
+ *
+ * @param records The records pending execution
+ * @param isRecordPop The direction that these records are being run.
+ */
+ private void removeRedundantOperationsAndExecute(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop) {
+ if (records == null || records.isEmpty()) {
+ return;
+ }
+
+ if (isRecordPop == null || records.size() != isRecordPop.size()) {
+ throw new IllegalStateException("Internal error with the back stack records");
+ }
+
+ // Force start of any postponed transactions that interact with scheduled transactions:
+ executePostponedTransaction(records, isRecordPop);
+
+ final int numRecords = records.size();
+ int startIndex = 0;
+ for (int recordNum = 0; recordNum < numRecords; recordNum++) {
+ final boolean canReorder = records.get(recordNum).mReorderingAllowed;
+ if (!canReorder) {
+ // execute all previous transactions
+ if (startIndex != recordNum) {
+ executeOpsTogether(records, isRecordPop, startIndex, recordNum);
+ }
+ // execute all pop operations that don't allow reordering together or
+ // one add operation
+ int reorderingEnd = recordNum + 1;
+ if (isRecordPop.get(recordNum)) {
+ while (reorderingEnd < numRecords
+ && isRecordPop.get(reorderingEnd)
+ && !records.get(reorderingEnd).mReorderingAllowed) {
+ reorderingEnd++;
+ }
+ }
+ executeOpsTogether(records, isRecordPop, recordNum, reorderingEnd);
+ startIndex = reorderingEnd;
+ recordNum = reorderingEnd - 1;
+ }
+ }
+ if (startIndex != numRecords) {
+ executeOpsTogether(records, isRecordPop, startIndex, numRecords);
+ }
+ }
+
+ /**
+ * Executes a subset of a list of BackStackRecords, all of which either allow reordering or
+ * do not allow ordering.
+ * @param records A list of BackStackRecords that are to be executed together
+ * @param isRecordPop The direction that these records are being run.
+ * @param startIndex The index of the first record in <code>records</code> to be executed
+ * @param endIndex One more than the final record index in <code>records</code> to executed.
+ */
+ private void executeOpsTogether(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+ final boolean allowReordering = records.get(startIndex).mReorderingAllowed;
+ boolean addToBackStack = false;
+ if (mTmpAddedFragments == null) {
+ mTmpAddedFragments = new ArrayList<>();
+ } else {
+ mTmpAddedFragments.clear();
+ }
+ mTmpAddedFragments.addAll(mAdded);
+ Fragment oldPrimaryNav = getPrimaryNavigationFragment();
+ for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+ final BackStackRecord record = records.get(recordNum);
+ final boolean isPop = isRecordPop.get(recordNum);
+ if (!isPop) {
+ oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav);
+ } else {
+ record.trackAddedFragmentsInPop(mTmpAddedFragments);
+ }
+ addToBackStack = addToBackStack || record.mAddToBackStack;
+ }
+ mTmpAddedFragments.clear();
+
+ if (!allowReordering) {
+ FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex,
+ false);
+ }
+ executeOps(records, isRecordPop, startIndex, endIndex);
+
+ int postponeIndex = endIndex;
+ if (allowReordering) {
+ ArraySet<Fragment> addedFragments = new ArraySet<>();
+ addAddedFragments(addedFragments);
+ postponeIndex = postponePostponableTransactions(records, isRecordPop,
+ startIndex, endIndex, addedFragments);
+ makeRemovedFragmentsInvisible(addedFragments);
+ }
+
+ if (postponeIndex != startIndex && allowReordering) {
+ // need to run something now
+ FragmentTransition.startTransitions(this, records, isRecordPop, startIndex,
+ postponeIndex, true);
+ moveToState(mCurState, true);
+ }
+
+ for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+ final BackStackRecord record = records.get(recordNum);
+ final boolean isPop = isRecordPop.get(recordNum);
+ if (isPop && record.mIndex >= 0) {
+ freeBackStackIndex(record.mIndex);
+ record.mIndex = -1;
+ }
+ record.runOnCommitRunnables();
+ }
+
+ if (addToBackStack) {
+ reportBackStackChanged();
+ }
+ }
+
+ /**
+ * Any fragments that were removed because they have been postponed should have their views
+ * made invisible by setting their transition alpha to 0.
+ *
+ * @param fragments The fragments that were added during operation execution. Only the ones
+ * that are no longer added will have their transition alpha changed.
+ */
+ private void makeRemovedFragmentsInvisible(ArraySet<Fragment> fragments) {
+ final int numAdded = fragments.size();
+ for (int i = 0; i < numAdded; i++) {
+ final Fragment fragment = fragments.valueAt(i);
+ if (!fragment.mAdded) {
+ final View view = fragment.getView();
+ view.setTransitionAlpha(0f);
+ }
+ }
+ }
+
+ /**
+ * Examine all transactions and determine which ones are marked as postponed. Those will
+ * have their operations rolled back and moved to the end of the record list (up to endIndex).
+ * It will also add the postponed transaction to the queue.
+ *
+ * @param records A list of BackStackRecords that should be checked.
+ * @param isRecordPop The direction that these records are being run.
+ * @param startIndex The index of the first record in <code>records</code> to be checked
+ * @param endIndex One more than the final record index in <code>records</code> to be checked.
+ * @return The index of the first postponed transaction or endIndex if no transaction was
+ * postponed.
+ */
+ private int postponePostponableTransactions(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex,
+ ArraySet<Fragment> added) {
+ int postponeIndex = endIndex;
+ for (int i = endIndex - 1; i >= startIndex; i--) {
+ final BackStackRecord record = records.get(i);
+ final boolean isPop = isRecordPop.get(i);
+ boolean isPostponed = record.isPostponed() &&
+ !record.interactsWith(records, i + 1, endIndex);
+ if (isPostponed) {
+ if (mPostponedTransactions == null) {
+ mPostponedTransactions = new ArrayList<>();
+ }
+ StartEnterTransitionListener listener =
+ new StartEnterTransitionListener(record, isPop);
+ mPostponedTransactions.add(listener);
+ record.setOnStartPostponedListener(listener);
+
+ // roll back the transaction
+ if (isPop) {
+ record.executeOps();
+ } else {
+ record.executePopOps(false);
+ }
+
+ // move to the end
+ postponeIndex--;
+ if (i != postponeIndex) {
+ records.remove(i);
+ records.add(postponeIndex, record);
+ }
+
+ // different views may be visible now
+ addAddedFragments(added);
+ }
+ }
+ return postponeIndex;
+ }
+
+ /**
+ * When a postponed transaction is ready to be started, this completes the transaction,
+ * removing, hiding, or showing views as well as starting the animations and transitions.
+ * <p>
+ * {@code runtransitions} is set to false when the transaction postponement was interrupted
+ * abnormally -- normally by a new transaction being started that affects the postponed
+ * transaction.
+ *
+ * @param record The transaction to run
+ * @param isPop true if record is popping or false if it is adding
+ * @param runTransitions true if the fragment transition should be run or false otherwise.
+ * @param moveToState true if the state should be changed after executing the operations.
+ * This is false when the transaction is canceled when a postponed
+ * transaction is popped.
+ */
+ private void completeExecute(BackStackRecord record, boolean isPop, boolean runTransitions,
+ boolean moveToState) {
+ if (isPop) {
+ record.executePopOps(moveToState);
+ } else {
+ record.executeOps();
+ }
+ ArrayList<BackStackRecord> records = new ArrayList<>(1);
+ ArrayList<Boolean> isRecordPop = new ArrayList<>(1);
+ records.add(record);
+ isRecordPop.add(isPop);
+ if (runTransitions) {
+ FragmentTransition.startTransitions(this, records, isRecordPop, 0, 1, true);
+ }
+ if (moveToState) {
+ moveToState(mCurState, true);
+ }
+
+ if (mActive != null) {
+ final int numActive = mActive.size();
+ for (int i = 0; i < numActive; i++) {
+ // Allow added fragments to be removed during the pop since we aren't going
+ // to move them to the final state with moveToState(mCurState).
+ Fragment fragment = mActive.valueAt(i);
+ if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded
+ && record.interactsWith(fragment.mContainerId)) {
+ fragment.mIsNewlyAdded = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Find a fragment within the fragment's container whose View should be below the passed
+ * fragment. {@code null} is returned when the fragment has no View or if there should be
+ * no fragment with a View below the given fragment.
+ *
+ * As an example, if mAdded has two Fragments with Views sharing the same container:
+ * FragmentA
+ * FragmentB
+ *
+ * Then, when processing FragmentB, FragmentA will be returned. If, however, FragmentA
+ * had no View, null would be returned.
+ *
+ * @param f The fragment that may be on top of another fragment.
+ * @return The fragment with a View under f, if one exists or null if f has no View or
+ * there are no fragments with Views in the same container.
+ */
+ private Fragment findFragmentUnder(Fragment f) {
+ final ViewGroup container = f.mContainer;
+ final View view = f.mView;
+
+ if (container == null || view == null) {
+ return null;
+ }
+
+ final int fragmentIndex = mAdded.indexOf(f);
+ for (int i = fragmentIndex - 1; i >= 0; i--) {
+ Fragment underFragment = mAdded.get(i);
+ if (underFragment.mContainer == container && underFragment.mView != null) {
+ // Found the fragment under this one
+ return underFragment;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Run the operations in the BackStackRecords, either to push or pop.
+ *
+ * @param records The list of records whose operations should be run.
+ * @param isRecordPop The direction that these records are being run.
+ * @param startIndex The index of the first entry in records to run.
+ * @param endIndex One past the index of the final entry in records to run.
+ */
+ private static void executeOps(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+ for (int i = startIndex; i < endIndex; i++) {
+ final BackStackRecord record = records.get(i);
+ final boolean isPop = isRecordPop.get(i);
+ if (isPop) {
+ record.bumpBackStackNesting(-1);
+ // Only execute the add operations at the end of
+ // all transactions.
+ boolean moveToState = i == (endIndex - 1);
+ record.executePopOps(moveToState);
+ } else {
+ record.bumpBackStackNesting(1);
+ record.executeOps();
+ }
+ }
+ }
+
+ /**
+ * Ensure that fragments that are added are moved to at least the CREATED state.
+ * Any newly-added Views are inserted into {@code added} so that the Transaction can be
+ * postponed with {@link Fragment#postponeEnterTransition()}. They will later be made
+ * invisible by changing their transitionAlpha to 0 if they have been removed when postponed.
+ */
+ private void addAddedFragments(ArraySet<Fragment> added) {
+ if (mCurState < Fragment.CREATED) {
+ return;
+ }
+ // We want to leave the fragment in the started state
+ final int state = Math.min(mCurState, Fragment.STARTED);
+ final int numAdded = mAdded.size();
+ for (int i = 0; i < numAdded; i++) {
+ Fragment fragment = mAdded.get(i);
+ if (fragment.mState < state) {
+ moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(), false);
+ if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) {
+ added.add(fragment);
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts all postponed transactions regardless of whether they are ready or not.
+ */
+ private void forcePostponedTransactions() {
+ if (mPostponedTransactions != null) {
+ while (!mPostponedTransactions.isEmpty()) {
+ mPostponedTransactions.remove(0).completeTransaction();
+ }
+ }
+ }
+
+ /**
+ * Ends the animations of fragments so that they immediately reach the end state.
+ * This is used prior to saving the state so that the correct state is saved.
+ */
+ private void endAnimatingAwayFragments() {
+ final int numFragments = mActive == null ? 0 : mActive.size();
+ for (int i = 0; i < numFragments; i++) {
+ Fragment fragment = mActive.valueAt(i);
+ if (fragment != null && fragment.getAnimatingAway() != null) {
+ // Give up waiting for the animation and just end it.
+ fragment.getAnimatingAway().end();
+ }
+ }
+ }
+
+ /**
+ * Adds all records in the pending actions to records and whether they are add or pop
+ * operations to isPop. After executing, the pending actions will be empty.
+ *
+ * @param records All pending actions will generate BackStackRecords added to this.
+ * This contains the transactions, in order, to execute.
+ * @param isPop All pending actions will generate booleans to add to this. This contains
+ * an entry for each entry in records to indicate whether or not it is a
+ * pop action.
+ */
+ private boolean generateOpsForPendingActions(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isPop) {
+ boolean didSomething = false;
+ synchronized (this) {
+ if (mPendingActions == null || mPendingActions.size() == 0) {
+ return false;
+ }
+
+ final int numActions = mPendingActions.size();
+ for (int i = 0; i < numActions; i++) {
+ didSomething |= mPendingActions.get(i).generateOps(records, isPop);
+ }
+ mPendingActions.clear();
+ mHost.getHandler().removeCallbacks(mExecCommit);
+ }
+ return didSomething;
+ }
+
+ void doPendingDeferredStart() {
+ if (mHavePendingDeferredStart) {
+ boolean loadersRunning = false;
+ for (int i=0; i<mActive.size(); i++) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null && f.mLoaderManager != null) {
+ loadersRunning |= f.mLoaderManager.hasRunningLoaders();
+ }
+ }
+ if (!loadersRunning) {
+ mHavePendingDeferredStart = false;
+ startPendingDeferredFragments();
+ }
+ }
+ }
+
+ void reportBackStackChanged() {
+ if (mBackStackChangeListeners != null) {
+ for (int i=0; i<mBackStackChangeListeners.size(); i++) {
+ mBackStackChangeListeners.get(i).onBackStackChanged();
+ }
+ }
+ }
+
+ void addBackStackState(BackStackRecord state) {
+ if (mBackStack == null) {
+ mBackStack = new ArrayList<BackStackRecord>();
+ }
+ mBackStack.add(state);
+ }
+
+ boolean popBackStackState(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
+ String name, int id, int flags) {
+ if (mBackStack == null) {
+ return false;
+ }
+ if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) {
+ int last = mBackStack.size() - 1;
+ if (last < 0) {
+ return false;
+ }
+ records.add(mBackStack.remove(last));
+ isRecordPop.add(true);
+ } else {
+ int index = -1;
+ if (name != null || id >= 0) {
+ // If a name or ID is specified, look for that place in
+ // the stack.
+ index = mBackStack.size()-1;
+ while (index >= 0) {
+ BackStackRecord bss = mBackStack.get(index);
+ if (name != null && name.equals(bss.getName())) {
+ break;
+ }
+ if (id >= 0 && id == bss.mIndex) {
+ break;
+ }
+ index--;
+ }
+ if (index < 0) {
+ return false;
+ }
+ if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) {
+ index--;
+ // Consume all following entries that match.
+ while (index >= 0) {
+ BackStackRecord bss = mBackStack.get(index);
+ if ((name != null && name.equals(bss.getName()))
+ || (id >= 0 && id == bss.mIndex)) {
+ index--;
+ continue;
+ }
+ break;
+ }
+ }
+ }
+ if (index == mBackStack.size()-1) {
+ return false;
+ }
+ for (int i = mBackStack.size() - 1; i > index; i--) {
+ records.add(mBackStack.remove(i));
+ isRecordPop.add(true);
+ }
+ }
+ return true;
+ }
+
+ FragmentManagerNonConfig retainNonConfig() {
+ setRetaining(mSavedNonConfig);
+ return mSavedNonConfig;
+ }
+
+ /**
+ * Recurse the FragmentManagerNonConfig fragments and set the mRetaining to true. This
+ * was previously done while saving the non-config state, but that has been moved to
+ * {@link #saveNonConfig()} called from {@link #saveAllState()}. If mRetaining is set too
+ * early, the fragment won't be destroyed when the FragmentManager is destroyed.
+ */
+ private static void setRetaining(FragmentManagerNonConfig nonConfig) {
+ if (nonConfig == null) {
+ return;
+ }
+ List<Fragment> fragments = nonConfig.getFragments();
+ if (fragments != null) {
+ for (Fragment fragment : fragments) {
+ fragment.mRetaining = true;
+ }
+ }
+ List<FragmentManagerNonConfig> children = nonConfig.getChildNonConfigs();
+ if (children != null) {
+ for (FragmentManagerNonConfig child : children) {
+ setRetaining(child);
+ }
+ }
+ }
+
+ void saveNonConfig() {
+ ArrayList<Fragment> fragments = null;
+ ArrayList<FragmentManagerNonConfig> childFragments = null;
+ if (mActive != null) {
+ for (int i=0; i<mActive.size(); i++) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null) {
+ if (f.mRetainInstance) {
+ if (fragments == null) {
+ fragments = new ArrayList<>();
+ }
+ fragments.add(f);
+ f.mTargetIndex = f.mTarget != null ? f.mTarget.mIndex : -1;
+ if (DEBUG) Log.v(TAG, "retainNonConfig: keeping retained " + f);
+ }
+ FragmentManagerNonConfig child;
+ if (f.mChildFragmentManager != null) {
+ f.mChildFragmentManager.saveNonConfig();
+ child = f.mChildFragmentManager.mSavedNonConfig;
+ } else {
+ // f.mChildNonConfig may be not null, when the parent fragment is
+ // in the backstack.
+ child = f.mChildNonConfig;
+ }
+
+ if (childFragments == null && child != null) {
+ childFragments = new ArrayList<>(mActive.size());
+ for (int j = 0; j < i; j++) {
+ childFragments.add(null);
+ }
+ }
+
+ if (childFragments != null) {
+ childFragments.add(child);
+ }
+ }
+ }
+ }
+ if (fragments == null && childFragments == null) {
+ mSavedNonConfig = null;
+ } else {
+ mSavedNonConfig = new FragmentManagerNonConfig(fragments, childFragments);
+ }
+ }
+
+ void saveFragmentViewState(Fragment f) {
+ if (f.mView == null) {
+ return;
+ }
+ if (mStateArray == null) {
+ mStateArray = new SparseArray<Parcelable>();
+ } else {
+ mStateArray.clear();
+ }
+ f.mView.saveHierarchyState(mStateArray);
+ if (mStateArray.size() > 0) {
+ f.mSavedViewState = mStateArray;
+ mStateArray = null;
+ }
+ }
+
+ Bundle saveFragmentBasicState(Fragment f) {
+ Bundle result = null;
+
+ if (mStateBundle == null) {
+ mStateBundle = new Bundle();
+ }
+ f.performSaveInstanceState(mStateBundle);
+ dispatchOnFragmentSaveInstanceState(f, mStateBundle, false);
+ if (!mStateBundle.isEmpty()) {
+ result = mStateBundle;
+ mStateBundle = null;
+ }
+
+ if (f.mView != null) {
+ saveFragmentViewState(f);
+ }
+ if (f.mSavedViewState != null) {
+ if (result == null) {
+ result = new Bundle();
+ }
+ result.putSparseParcelableArray(
+ FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState);
+ }
+ if (!f.mUserVisibleHint) {
+ if (result == null) {
+ result = new Bundle();
+ }
+ // Only add this if it's not the default value
+ result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint);
+ }
+
+ return result;
+ }
+
+ Parcelable saveAllState() {
+ // Make sure all pending operations have now been executed to get
+ // our state update-to-date.
+ forcePostponedTransactions();
+ endAnimatingAwayFragments();
+ execPendingActions();
+
+ mStateSaved = true;
+ mSavedNonConfig = null;
+
+ if (mActive == null || mActive.size() <= 0) {
+ return null;
+ }
+
+ // First collect all active fragments.
+ int N = mActive.size();
+ FragmentState[] active = new FragmentState[N];
+ boolean haveFragments = false;
+ for (int i=0; i<N; i++) {
+ Fragment f = mActive.valueAt(i);
+ if (f != null) {
+ if (f.mIndex < 0) {
+ throwException(new IllegalStateException(
+ "Failure saving state: active " + f
+ + " has cleared index: " + f.mIndex));
+ }
+
+ haveFragments = true;
+
+ FragmentState fs = new FragmentState(f);
+ active[i] = fs;
+
+ if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
+ fs.mSavedFragmentState = saveFragmentBasicState(f);
+
+ if (f.mTarget != null) {
+ if (f.mTarget.mIndex < 0) {
+ throwException(new IllegalStateException(
+ "Failure saving state: " + f
+ + " has target not in fragment manager: " + f.mTarget));
+ }
+ if (fs.mSavedFragmentState == null) {
+ fs.mSavedFragmentState = new Bundle();
+ }
+ putFragment(fs.mSavedFragmentState,
+ FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget);
+ if (f.mTargetRequestCode != 0) {
+ fs.mSavedFragmentState.putInt(
+ FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,
+ f.mTargetRequestCode);
+ }
+ }
+
+ } else {
+ fs.mSavedFragmentState = f.mSavedFragmentState;
+ }
+
+ if (DEBUG) Log.v(TAG, "Saved state of " + f + ": "
+ + fs.mSavedFragmentState);
+ }
+ }
+
+ if (!haveFragments) {
+ if (DEBUG) Log.v(TAG, "saveAllState: no fragments!");
+ return null;
+ }
+
+ int[] added = null;
+ BackStackState[] backStack = null;
+
+ // Build list of currently added fragments.
+ N = mAdded.size();
+ if (N > 0) {
+ added = new int[N];
+ for (int i=0; i<N; i++) {
+ added[i] = mAdded.get(i).mIndex;
+ if (added[i] < 0) {
+ throwException(new IllegalStateException(
+ "Failure saving state: active " + mAdded.get(i)
+ + " has cleared index: " + added[i]));
+ }
+ if (DEBUG) Log.v(TAG, "saveAllState: adding fragment #" + i
+ + ": " + mAdded.get(i));
+ }
+ }
+
+ // Now save back stack.
+ if (mBackStack != null) {
+ N = mBackStack.size();
+ if (N > 0) {
+ backStack = new BackStackState[N];
+ for (int i=0; i<N; i++) {
+ backStack[i] = new BackStackState(this, mBackStack.get(i));
+ if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i
+ + ": " + mBackStack.get(i));
+ }
+ }
+ }
+
+ FragmentManagerState fms = new FragmentManagerState();
+ fms.mActive = active;
+ fms.mAdded = added;
+ fms.mBackStack = backStack;
+ fms.mNextFragmentIndex = mNextFragmentIndex;
+ if (mPrimaryNav != null) {
+ fms.mPrimaryNavActiveIndex = mPrimaryNav.mIndex;
+ }
+ saveNonConfig();
+ return fms;
+ }
+
+ void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
+ // If there is no saved state at all, then there can not be
+ // any nonConfig fragments either, so that is that.
+ if (state == null) return;
+ FragmentManagerState fms = (FragmentManagerState)state;
+ if (fms.mActive == null) return;
+
+ List<FragmentManagerNonConfig> childNonConfigs = null;
+
+ // First re-attach any non-config instances we are retaining back
+ // to their saved state, so we don't try to instantiate them again.
+ if (nonConfig != null) {
+ List<Fragment> nonConfigFragments = nonConfig.getFragments();
+ childNonConfigs = nonConfig.getChildNonConfigs();
+ final int count = nonConfigFragments != null ? nonConfigFragments.size() : 0;
+ for (int i = 0; i < count; i++) {
+ Fragment f = nonConfigFragments.get(i);
+ if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
+ int index = 0; // index of f in fms.mActive
+ while (index < fms.mActive.length && fms.mActive[index].mIndex != f.mIndex) {
+ index++;
+ }
+ if (index == fms.mActive.length) {
+ throwException(new IllegalStateException("Could not find active fragment "
+ + "with index " + f.mIndex));
+ }
+ FragmentState fs = fms.mActive[index];
+ fs.mInstance = f;
+ f.mSavedViewState = null;
+ f.mBackStackNesting = 0;
+ f.mInLayout = false;
+ f.mAdded = false;
+ f.mTarget = null;
+ if (fs.mSavedFragmentState != null) {
+ fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());
+ f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
+ FragmentManagerImpl.VIEW_STATE_TAG);
+ f.mSavedFragmentState = fs.mSavedFragmentState;
+ }
+ }
+ }
+
+ // Build the full list of active fragments, instantiating them from
+ // their saved state.
+ mActive = new SparseArray<>(fms.mActive.length);
+ for (int i=0; i<fms.mActive.length; i++) {
+ FragmentState fs = fms.mActive[i];
+ if (fs != null) {
+ FragmentManagerNonConfig childNonConfig = null;
+ if (childNonConfigs != null && i < childNonConfigs.size()) {
+ childNonConfig = childNonConfigs.get(i);
+ }
+ Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig);
+ if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
+ mActive.put(f.mIndex, f);
+ // Now that the fragment is instantiated (or came from being
+ // retained above), clear mInstance in case we end up re-restoring
+ // from this FragmentState again.
+ fs.mInstance = null;
+ }
+ }
+
+ // Update the target of all retained fragments.
+ if (nonConfig != null) {
+ List<Fragment> nonConfigFragments = nonConfig.getFragments();
+ final int count = nonConfigFragments != null ? nonConfigFragments.size() : 0;
+ for (int i = 0; i < count; i++) {
+ Fragment f = nonConfigFragments.get(i);
+ if (f.mTargetIndex >= 0) {
+ f.mTarget = mActive.get(f.mTargetIndex);
+ if (f.mTarget == null) {
+ Log.w(TAG, "Re-attaching retained fragment " + f
+ + " target no longer exists: " + f.mTargetIndex);
+ f.mTarget = null;
+ }
+ }
+ }
+ }
+
+ // Build the list of currently added fragments.
+ mAdded.clear();
+ if (fms.mAdded != null) {
+ for (int i=0; i<fms.mAdded.length; i++) {
+ Fragment f = mActive.get(fms.mAdded[i]);
+ if (f == null) {
+ throwException(new IllegalStateException(
+ "No instantiated fragment for index #" + fms.mAdded[i]));
+ }
+ f.mAdded = true;
+ if (DEBUG) Log.v(TAG, "restoreAllState: added #" + i + ": " + f);
+ if (mAdded.contains(f)) {
+ throw new IllegalStateException("Already added!");
+ }
+ synchronized (mAdded) {
+ mAdded.add(f);
+ }
+ }
+ }
+
+ // Build the back stack.
+ if (fms.mBackStack != null) {
+ mBackStack = new ArrayList<BackStackRecord>(fms.mBackStack.length);
+ for (int i=0; i<fms.mBackStack.length; i++) {
+ BackStackRecord bse = fms.mBackStack[i].instantiate(this);
+ if (DEBUG) {
+ Log.v(TAG, "restoreAllState: back stack #" + i
+ + " (index " + bse.mIndex + "): " + bse);
+ LogWriter logw = new LogWriter(Log.VERBOSE, TAG);
+ PrintWriter pw = new FastPrintWriter(logw, false, 1024);
+ bse.dump(" ", pw, false);
+ pw.flush();
+ }
+ mBackStack.add(bse);
+ if (bse.mIndex >= 0) {
+ setBackStackIndex(bse.mIndex, bse);
+ }
+ }
+ } else {
+ mBackStack = null;
+ }
+
+ if (fms.mPrimaryNavActiveIndex >= 0) {
+ mPrimaryNav = mActive.get(fms.mPrimaryNavActiveIndex);
+ }
+
+ mNextFragmentIndex = fms.mNextFragmentIndex;
+ }
+
+ /**
+ * To prevent list modification errors, mActive sets values to null instead of
+ * removing them when the Fragment becomes inactive. This cleans up the list at the
+ * end of executing the transactions.
+ */
+ private void burpActive() {
+ if (mActive != null) {
+ for (int i = mActive.size() - 1; i >= 0; i--) {
+ if (mActive.valueAt(i) == null) {
+ mActive.delete(mActive.keyAt(i));
+ }
+ }
+ }
+ }
+
+ public void attachController(FragmentHostCallback<?> host, FragmentContainer container,
+ Fragment parent) {
+ if (mHost != null) throw new IllegalStateException("Already attached");
+ mHost = host;
+ mContainer = container;
+ mParent = parent;
+ mAllowOldReentrantBehavior = getTargetSdk() <= Build.VERSION_CODES.N_MR1;
+ }
+
+ /**
+ * @return the target SDK of the FragmentManager's application info. If the
+ * FragmentManager has been torn down, then 0 is returned.
+ */
+ int getTargetSdk() {
+ if (mHost != null) {
+ Context context = mHost.getContext();
+ if (context != null) {
+ ApplicationInfo info = context.getApplicationInfo();
+ if (info != null) {
+ return info.targetSdkVersion;
+ }
+ }
+ }
+ return 0;
+ }
+
+ public void noteStateNotSaved() {
+ mSavedNonConfig = null;
+ mStateSaved = false;
+ final int addedCount = mAdded.size();
+ for (int i = 0; i < addedCount; i++) {
+ Fragment fragment = mAdded.get(i);
+ if (fragment != null) {
+ fragment.noteStateNotSaved();
+ }
+ }
+ }
+
+ public void dispatchCreate() {
+ mStateSaved = false;
+ dispatchMoveToState(Fragment.CREATED);
+ }
+
+ public void dispatchActivityCreated() {
+ mStateSaved = false;
+ dispatchMoveToState(Fragment.ACTIVITY_CREATED);
+ }
+
+ public void dispatchStart() {
+ mStateSaved = false;
+ dispatchMoveToState(Fragment.STARTED);
+ }
+
+ public void dispatchResume() {
+ mStateSaved = false;
+ dispatchMoveToState(Fragment.RESUMED);
+ }
+
+ public void dispatchPause() {
+ dispatchMoveToState(Fragment.STARTED);
+ }
+
+ public void dispatchStop() {
+ dispatchMoveToState(Fragment.STOPPED);
+ }
+
+ public void dispatchDestroyView() {
+ dispatchMoveToState(Fragment.CREATED);
+ }
+
+ public void dispatchDestroy() {
+ mDestroyed = true;
+ execPendingActions();
+ dispatchMoveToState(Fragment.INITIALIZING);
+ mHost = null;
+ mContainer = null;
+ mParent = null;
+ }
+
+ /**
+ * This method is called by dispatch* methods to change the FragmentManager's state.
+ * It calls moveToState directly if the target SDK is older than O. Otherwise, it sets and
+ * clears mExecutingActions to ensure that there is no reentrancy while the
+ * FragmentManager is changing state.
+ *
+ * @param state The new state of the FragmentManager.
+ */
+ private void dispatchMoveToState(int state) {
+ if (mAllowOldReentrantBehavior) {
+ moveToState(state, false);
+ } else {
+ try {
+ mExecutingActions = true;
+ moveToState(state, false);
+ } finally {
+ mExecutingActions = false;
+ }
+ }
+ execPendingActions();
+ }
+
+ /**
+ * @deprecated use {@link #dispatchMultiWindowModeChanged(boolean, Configuration)}
+ */
+ @Deprecated
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ for (int i = mAdded.size() - 1; i >= 0; --i) {
+ final Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performMultiWindowModeChanged(isInMultiWindowMode);
+ }
+ }
+ }
+
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode,
+ Configuration newConfig) {
+ for (int i = mAdded.size() - 1; i >= 0; --i) {
+ final Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+ }
+ }
+
+ /**
+ * @deprecated use {@link #dispatchPictureInPictureModeChanged(boolean, Configuration)}
+ */
+ @Deprecated
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ for (int i = mAdded.size() - 1; i >= 0; --i) {
+ final Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+ }
+ }
+
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode,
+ Configuration newConfig) {
+ for (int i = mAdded.size() - 1; i >= 0; --i) {
+ final Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
+ }
+ }
+ }
+
+ public void dispatchConfigurationChanged(Configuration newConfig) {
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performConfigurationChanged(newConfig);
+ }
+ }
+ }
+
+ public void dispatchLowMemory() {
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performLowMemory();
+ }
+ }
+ }
+
+ public void dispatchTrimMemory(int level) {
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performTrimMemory(level);
+ }
+ }
+ }
+
+ public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (mCurState < Fragment.CREATED) {
+ return false;
+ }
+ boolean show = false;
+ ArrayList<Fragment> newMenus = null;
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ if (f.performCreateOptionsMenu(menu, inflater)) {
+ show = true;
+ if (newMenus == null) {
+ newMenus = new ArrayList<Fragment>();
+ }
+ newMenus.add(f);
+ }
+ }
+ }
+
+ if (mCreatedMenus != null) {
+ for (int i=0; i<mCreatedMenus.size(); i++) {
+ Fragment f = mCreatedMenus.get(i);
+ if (newMenus == null || !newMenus.contains(f)) {
+ f.onDestroyOptionsMenu();
+ }
+ }
+ }
+
+ mCreatedMenus = newMenus;
+
+ return show;
+ }
+
+ public boolean dispatchPrepareOptionsMenu(Menu menu) {
+ if (mCurState < Fragment.CREATED) {
+ return false;
+ }
+ boolean show = false;
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ if (f.performPrepareOptionsMenu(menu)) {
+ show = true;
+ }
+ }
+ }
+ return show;
+ }
+
+ public boolean dispatchOptionsItemSelected(MenuItem item) {
+ if (mCurState < Fragment.CREATED) {
+ return false;
+ }
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ if (f.performOptionsItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean dispatchContextItemSelected(MenuItem item) {
+ if (mCurState < Fragment.CREATED) {
+ return false;
+ }
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ if (f.performContextItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void dispatchOptionsMenuClosed(Menu menu) {
+ if (mCurState < Fragment.CREATED) {
+ return;
+ }
+ for (int i = 0; i < mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null) {
+ f.performOptionsMenuClosed(menu);
+ }
+ }
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public void setPrimaryNavigationFragment(Fragment f) {
+ if (f != null && (mActive.get(f.mIndex) != f
+ || (f.mHost != null && f.getFragmentManager() != this))) {
+ throw new IllegalArgumentException("Fragment " + f
+ + " is not an active fragment of FragmentManager " + this);
+ }
+ mPrimaryNav = f;
+ }
+
+ public Fragment getPrimaryNavigationFragment() {
+ return mPrimaryNav;
+ }
+
+ public void registerFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb,
+ boolean recursive) {
+ mLifecycleCallbacks.add(new Pair<>(cb, recursive));
+ }
+
+ public void unregisterFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb) {
+ synchronized (mLifecycleCallbacks) {
+ for (int i = 0, N = mLifecycleCallbacks.size(); i < N; i++) {
+ if (mLifecycleCallbacks.get(i).first == cb) {
+ mLifecycleCallbacks.remove(i);
+ break;
+ }
+ }
+ }
+ }
+
+ void dispatchOnFragmentPreAttached(Fragment f, Context context, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentPreAttached(f, context, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentPreAttached(this, f, context);
+ }
+ }
+ }
+
+ void dispatchOnFragmentAttached(Fragment f, Context context, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentAttached(f, context, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentAttached(this, f, context);
+ }
+ }
+ }
+
+ void dispatchOnFragmentPreCreated(Fragment f, Bundle savedInstanceState,
+ boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentPreCreated(f, savedInstanceState, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentPreCreated(this, f, savedInstanceState);
+ }
+ }
+ }
+
+ void dispatchOnFragmentCreated(Fragment f, Bundle savedInstanceState, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentCreated(f, savedInstanceState, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentCreated(this, f, savedInstanceState);
+ }
+ }
+ }
+
+ void dispatchOnFragmentActivityCreated(Fragment f, Bundle savedInstanceState,
+ boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentActivityCreated(f, savedInstanceState, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentActivityCreated(this, f, savedInstanceState);
+ }
+ }
+ }
+
+ void dispatchOnFragmentViewCreated(Fragment f, View v, Bundle savedInstanceState,
+ boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentViewCreated(f, v, savedInstanceState, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentViewCreated(this, f, v, savedInstanceState);
+ }
+ }
+ }
+
+ void dispatchOnFragmentStarted(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentStarted(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentStarted(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentResumed(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentResumed(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentResumed(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentPaused(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentPaused(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentPaused(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentStopped(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentStopped(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentStopped(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentSaveInstanceState(Fragment f, Bundle outState, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentSaveInstanceState(f, outState, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentSaveInstanceState(this, f, outState);
+ }
+ }
+ }
+
+ void dispatchOnFragmentViewDestroyed(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentViewDestroyed(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentViewDestroyed(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentDestroyed(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentDestroyed(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentDestroyed(this, f);
+ }
+ }
+ }
+
+ void dispatchOnFragmentDetached(Fragment f, boolean onlyRecursive) {
+ if (mParent != null) {
+ FragmentManager parentManager = mParent.getFragmentManager();
+ if (parentManager instanceof FragmentManagerImpl) {
+ ((FragmentManagerImpl) parentManager)
+ .dispatchOnFragmentDetached(f, true);
+ }
+ }
+ for (Pair<FragmentLifecycleCallbacks, Boolean> p : mLifecycleCallbacks) {
+ if (!onlyRecursive || p.second) {
+ p.first.onFragmentDetached(this, f);
+ }
+ }
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ if (mHost != null && mCurState == Fragment.RESUMED) {
+ mHost.onInvalidateOptionsMenu();
+ } else {
+ mNeedMenuInvalidate = true;
+ }
+ }
+
+ public static int reverseTransit(int transit) {
+ int rev = 0;
+ switch (transit) {
+ case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:
+ rev = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:
+ rev = FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
+ rev = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
+ break;
+ }
+ return rev;
+
+ }
+
+ public static int transitToStyleIndex(int transit, boolean enter) {
+ int animAttr = -1;
+ switch (transit) {
+ case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:
+ animAttr = enter
+ ? com.android.internal.R.styleable.FragmentAnimation_fragmentOpenEnterAnimation
+ : com.android.internal.R.styleable.FragmentAnimation_fragmentOpenExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.FragmentAnimation_fragmentCloseEnterAnimation
+ : com.android.internal.R.styleable.FragmentAnimation_fragmentCloseExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.FragmentAnimation_fragmentFadeEnterAnimation
+ : com.android.internal.R.styleable.FragmentAnimation_fragmentFadeExitAnimation;
+ break;
+ }
+ return animAttr;
+ }
+
+ @Override
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ if (!"fragment".equals(name)) {
+ return null;
+ }
+
+ String fname = attrs.getAttributeValue(null, "class");
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment);
+ if (fname == null) {
+ fname = a.getString(com.android.internal.R.styleable.Fragment_name);
+ }
+ int id = a.getResourceId(com.android.internal.R.styleable.Fragment_id, View.NO_ID);
+ String tag = a.getString(com.android.internal.R.styleable.Fragment_tag);
+ a.recycle();
+
+ int containerId = parent != null ? parent.getId() : 0;
+ if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {
+ throw new IllegalArgumentException(attrs.getPositionDescription()
+ + ": Must specify unique android:id, android:tag, or have a parent with"
+ + " an id for " + fname);
+ }
+
+ // If we restored from a previous state, we may already have
+ // instantiated this fragment from the state and should use
+ // that instance instead of making a new one.
+ Fragment fragment = id != View.NO_ID ? findFragmentById(id) : null;
+ if (fragment == null && tag != null) {
+ fragment = findFragmentByTag(tag);
+ }
+ if (fragment == null && containerId != View.NO_ID) {
+ fragment = findFragmentById(containerId);
+ }
+
+ if (FragmentManagerImpl.DEBUG) Log.v(TAG, "onCreateView: id=0x"
+ + Integer.toHexString(id) + " fname=" + fname
+ + " existing=" + fragment);
+ if (fragment == null) {
+ fragment = mContainer.instantiate(context, fname, null);
+ fragment.mFromLayout = true;
+ fragment.mFragmentId = id != 0 ? id : containerId;
+ fragment.mContainerId = containerId;
+ fragment.mTag = tag;
+ fragment.mInLayout = true;
+ fragment.mFragmentManager = this;
+ fragment.mHost = mHost;
+ fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);
+ addFragment(fragment, true);
+ } else if (fragment.mInLayout) {
+ // A fragment already exists and it is not one we restored from
+ // previous state.
+ throw new IllegalArgumentException(attrs.getPositionDescription()
+ + ": Duplicate id 0x" + Integer.toHexString(id)
+ + ", tag " + tag + ", or parent id 0x" + Integer.toHexString(containerId)
+ + " with another fragment for " + fname);
+ } else {
+ // This fragment was retained from a previous instance; get it
+ // going now.
+ fragment.mInLayout = true;
+ fragment.mHost = mHost;
+ // If this fragment is newly instantiated (either right now, or
+ // from last saved state), then give it the attributes to
+ // initialize itself.
+ if (!fragment.mRetaining) {
+ fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);
+ }
+ }
+
+ // If we haven't finished entering the CREATED state ourselves yet,
+ // push the inflated child fragment along. This will ensureInflatedFragmentView
+ // at the right phase of the lifecycle so that we will have mView populated
+ // for compliant fragments below.
+ if (mCurState < Fragment.CREATED && fragment.mFromLayout) {
+ moveToState(fragment, Fragment.CREATED, 0, 0, false);
+ } else {
+ moveToState(fragment);
+ }
+
+ if (fragment.mView == null) {
+ throw new IllegalStateException("Fragment " + fname
+ + " did not create a view.");
+ }
+ if (id != 0) {
+ fragment.mView.setId(id);
+ }
+ if (fragment.mView.getTag() == null) {
+ fragment.mView.setTag(tag);
+ }
+ return fragment.mView;
+ }
+
+ @Override
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return null;
+ }
+
+ LayoutInflater.Factory2 getLayoutInflaterFactory() {
+ return this;
+ }
+
+ /**
+ * An add or pop transaction to be scheduled for the UI thread.
+ */
+ interface OpGenerator {
+ /**
+ * Generate transactions to add to {@code records} and whether or not the transaction is
+ * an add or pop to {@code isRecordPop}.
+ *
+ * records and isRecordPop must be added equally so that each transaction in records
+ * matches the boolean for whether or not it is a pop in isRecordPop.
+ *
+ * @param records A list to add transactions to.
+ * @param isRecordPop A list to add whether or not the transactions added to records is
+ * a pop transaction.
+ * @return true if something was added or false otherwise.
+ */
+ boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop);
+ }
+
+ /**
+ * A pop operation OpGenerator. This will be run on the UI thread and will generate the
+ * transactions that will be popped if anything can be popped.
+ */
+ private class PopBackStackState implements OpGenerator {
+ final String mName;
+ final int mId;
+ final int mFlags;
+
+ public PopBackStackState(String name, int id, int flags) {
+ mName = name;
+ mId = id;
+ mFlags = flags;
+ }
+
+ @Override
+ public boolean generateOps(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop) {
+ if (mPrimaryNav != null // We have a primary nav fragment
+ && mId < 0 // No valid id (since they're local)
+ && mName == null) { // no name to pop to (since they're local)
+ final FragmentManager childManager = mPrimaryNav.mChildFragmentManager;
+ if (childManager != null && childManager.popBackStackImmediate()) {
+ // We didn't add any operations for this FragmentManager even though
+ // a child did do work.
+ return false;
+ }
+ }
+ return popBackStackState(records, isRecordPop, mName, mId, mFlags);
+ }
+ }
+
+ /**
+ * A listener for a postponed transaction. This waits until
+ * {@link Fragment#startPostponedEnterTransition()} is called or a transaction is started
+ * that interacts with this one, based on interactions with the fragment container.
+ */
+ static class StartEnterTransitionListener
+ implements Fragment.OnStartEnterTransitionListener {
+ private final boolean mIsBack;
+ private final BackStackRecord mRecord;
+ private int mNumPostponed;
+
+ public StartEnterTransitionListener(BackStackRecord record, boolean isBack) {
+ mIsBack = isBack;
+ mRecord = record;
+ }
+
+ /**
+ * Called from {@link Fragment#startPostponedEnterTransition()}, this decreases the
+ * number of Fragments that are postponed. This may cause the transaction to schedule
+ * to finish running and run transitions and animations.
+ */
+ @Override
+ public void onStartEnterTransition() {
+ mNumPostponed--;
+ if (mNumPostponed != 0) {
+ return;
+ }
+ mRecord.mManager.scheduleCommit();
+ }
+
+ /**
+ * Called from {@link Fragment#
+ * setOnStartEnterTransitionListener(Fragment.OnStartEnterTransitionListener)}, this
+ * increases the number of fragments that are postponed as part of this transaction.
+ */
+ @Override
+ public void startListening() {
+ mNumPostponed++;
+ }
+
+ /**
+ * @return true if there are no more postponed fragments as part of the transaction.
+ */
+ public boolean isReady() {
+ return mNumPostponed == 0;
+ }
+
+ /**
+ * Completes the transaction and start the animations and transitions. This may skip
+ * the transitions if this is called before all fragments have called
+ * {@link Fragment#startPostponedEnterTransition()}.
+ */
+ public void completeTransaction() {
+ final boolean canceled;
+ canceled = mNumPostponed > 0;
+ FragmentManagerImpl manager = mRecord.mManager;
+ final int numAdded = manager.mAdded.size();
+ for (int i = 0; i < numAdded; i++) {
+ final Fragment fragment = manager.mAdded.get(i);
+ fragment.setOnStartEnterTransitionListener(null);
+ if (canceled && fragment.isPostponed()) {
+ fragment.startPostponedEnterTransition();
+ }
+ }
+ mRecord.mManager.completeExecute(mRecord, mIsBack, !canceled, true);
+ }
+
+ /**
+ * Cancels this transaction instead of completing it. That means that the state isn't
+ * changed, so the pop results in no change to the state.
+ */
+ public void cancelTransaction() {
+ mRecord.mManager.completeExecute(mRecord, mIsBack, false, false);
+ }
+ }
+}
diff --git a/android/app/FragmentManagerNonConfig.java b/android/app/FragmentManagerNonConfig.java
new file mode 100644
index 00000000..50d3797d
--- /dev/null
+++ b/android/app/FragmentManagerNonConfig.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.app;
+
+import java.util.List;
+
+/**
+ * FragmentManagerNonConfig stores the retained instance fragments across
+ * activity recreation events.
+ *
+ * <p>Apps should treat objects of this type as opaque, returned by
+ * and passed to the state save and restore process for fragments in
+ * {@link FragmentController#retainNonConfig()} and
+ * {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p>
+ */
+public class FragmentManagerNonConfig {
+ private final List<Fragment> mFragments;
+ private final List<FragmentManagerNonConfig> mChildNonConfigs;
+
+ FragmentManagerNonConfig(List<Fragment> fragments,
+ List<FragmentManagerNonConfig> childNonConfigs) {
+ mFragments = fragments;
+ mChildNonConfigs = childNonConfigs;
+ }
+
+ /**
+ * @return the retained instance fragments returned by a FragmentManager
+ */
+ List<Fragment> getFragments() {
+ return mFragments;
+ }
+
+ /**
+ * @return the FragmentManagerNonConfigs from any applicable fragment's child FragmentManager
+ */
+ List<FragmentManagerNonConfig> getChildNonConfigs() {
+ return mChildNonConfigs;
+ }
+}
diff --git a/android/app/FragmentState.java b/android/app/FragmentState.java
new file mode 100644
index 00000000..a61da3bb
--- /dev/null
+++ b/android/app/FragmentState.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+final class FragmentState implements Parcelable {
+ final String mClassName;
+ final int mIndex;
+ final boolean mFromLayout;
+ final int mFragmentId;
+ final int mContainerId;
+ final String mTag;
+ final boolean mRetainInstance;
+ final boolean mDetached;
+ final Bundle mArguments;
+ final boolean mHidden;
+
+ Bundle mSavedFragmentState;
+
+ Fragment mInstance;
+
+ FragmentState(Fragment frag) {
+ mClassName = frag.getClass().getName();
+ mIndex = frag.mIndex;
+ mFromLayout = frag.mFromLayout;
+ mFragmentId = frag.mFragmentId;
+ mContainerId = frag.mContainerId;
+ mTag = frag.mTag;
+ mRetainInstance = frag.mRetainInstance;
+ mDetached = frag.mDetached;
+ mArguments = frag.mArguments;
+ mHidden = frag.mHidden;
+ }
+
+ FragmentState(Parcel in) {
+ mClassName = in.readString();
+ mIndex = in.readInt();
+ mFromLayout = in.readInt() != 0;
+ mFragmentId = in.readInt();
+ mContainerId = in.readInt();
+ mTag = in.readString();
+ mRetainInstance = in.readInt() != 0;
+ mDetached = in.readInt() != 0;
+ mArguments = in.readBundle();
+ mHidden = in.readInt() != 0;
+ mSavedFragmentState = in.readBundle();
+ }
+
+ public Fragment instantiate(FragmentHostCallback host, FragmentContainer container,
+ Fragment parent, FragmentManagerNonConfig childNonConfig) {
+ if (mInstance == null) {
+ final Context context = host.getContext();
+ if (mArguments != null) {
+ mArguments.setClassLoader(context.getClassLoader());
+ }
+
+ if (container != null) {
+ mInstance = container.instantiate(context, mClassName, mArguments);
+ } else {
+ mInstance = Fragment.instantiate(context, mClassName, mArguments);
+ }
+
+ if (mSavedFragmentState != null) {
+ mSavedFragmentState.setClassLoader(context.getClassLoader());
+ mInstance.mSavedFragmentState = mSavedFragmentState;
+ }
+ mInstance.setIndex(mIndex, parent);
+ mInstance.mFromLayout = mFromLayout;
+ mInstance.mRestored = true;
+ mInstance.mFragmentId = mFragmentId;
+ mInstance.mContainerId = mContainerId;
+ mInstance.mTag = mTag;
+ mInstance.mRetainInstance = mRetainInstance;
+ mInstance.mDetached = mDetached;
+ mInstance.mHidden = mHidden;
+ mInstance.mFragmentManager = host.mFragmentManager;
+
+ if (FragmentManagerImpl.DEBUG) {
+ Log.v(FragmentManagerImpl.TAG, "Instantiated fragment " + mInstance);
+ }
+ }
+ mInstance.mChildNonConfig = childNonConfig;
+ return mInstance;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mClassName);
+ dest.writeInt(mIndex);
+ dest.writeInt(mFromLayout ? 1 : 0);
+ dest.writeInt(mFragmentId);
+ dest.writeInt(mContainerId);
+ dest.writeString(mTag);
+ dest.writeInt(mRetainInstance ? 1 : 0);
+ dest.writeInt(mDetached ? 1 : 0);
+ dest.writeBundle(mArguments);
+ dest.writeInt(mHidden ? 1 : 0);
+ dest.writeBundle(mSavedFragmentState);
+ }
+
+ public static final Parcelable.Creator<FragmentState> CREATOR =
+ new Parcelable.Creator<FragmentState>() {
+ @Override
+ public FragmentState createFromParcel(Parcel in) {
+ return new FragmentState(in);
+ }
+
+ @Override
+ public FragmentState[] newArray(int size) {
+ return new FragmentState[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/android/app/FragmentTransaction.java b/android/app/FragmentTransaction.java
new file mode 100644
index 00000000..c910e903
--- /dev/null
+++ b/android/app/FragmentTransaction.java
@@ -0,0 +1,402 @@
+package android.app;
+
+import android.annotation.AnimatorRes;
+import android.annotation.IdRes;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.os.Bundle;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * API for performing a set of Fragment operations.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using fragments, read the
+ * <a href="{@docRoot}guide/components/fragments.html">Fragments</a> developer
+ * guide.</p>
+ * </div>
+ */
+public abstract class FragmentTransaction {
+ /**
+ * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId.
+ */
+ public abstract FragmentTransaction add(Fragment fragment, String tag);
+
+ /**
+ * Calls {@link #add(int, Fragment, String)} with a null tag.
+ */
+ public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment);
+
+ /**
+ * Add a fragment to the activity state. This fragment may optionally
+ * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView}
+ * returns non-null) inserted into a container view of the activity.
+ *
+ * @param containerViewId Optional identifier of the container this fragment is
+ * to be placed in. If 0, it will not be placed in a container.
+ * @param fragment The fragment to be added. This fragment must not already
+ * be added to the activity.
+ * @param tag Optional tag name for the fragment, to later retrieve the
+ * fragment with {@link FragmentManager#findFragmentByTag(String)
+ * FragmentManager.findFragmentByTag(String)}.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment,
+ String tag);
+
+ /**
+ * Calls {@link #replace(int, Fragment, String)} with a null tag.
+ */
+ public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment);
+
+ /**
+ * Replace an existing fragment that was added to a container. This is
+ * essentially the same as calling {@link #remove(Fragment)} for all
+ * currently added fragments that were added with the same containerViewId
+ * and then {@link #add(int, Fragment, String)} with the same arguments
+ * given here.
+ *
+ * @param containerViewId Identifier of the container whose fragment(s) are
+ * to be replaced.
+ * @param fragment The new fragment to place in the container.
+ * @param tag Optional tag name for the fragment, to later retrieve the
+ * fragment with {@link FragmentManager#findFragmentByTag(String)
+ * FragmentManager.findFragmentByTag(String)}.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment,
+ String tag);
+
+ /**
+ * Remove an existing fragment. If it was added to a container, its view
+ * is also removed from that container.
+ *
+ * @param fragment The fragment to be removed.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction remove(Fragment fragment);
+
+ /**
+ * Hides an existing fragment. This is only relevant for fragments whose
+ * views have been added to a container, as this will cause the view to
+ * be hidden.
+ *
+ * @param fragment The fragment to be hidden.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction hide(Fragment fragment);
+
+ /**
+ * Shows a previously hidden fragment. This is only relevant for fragments whose
+ * views have been added to a container, as this will cause the view to
+ * be shown.
+ *
+ * @param fragment The fragment to be shown.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction show(Fragment fragment);
+
+ /**
+ * Detach the given fragment from the UI. This is the same state as
+ * when it is put on the back stack: the fragment is removed from
+ * the UI, however its state is still being actively managed by the
+ * fragment manager. When going into this state its view hierarchy
+ * is destroyed.
+ *
+ * @param fragment The fragment to be detached.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction detach(Fragment fragment);
+
+ /**
+ * Re-attach a fragment after it had previously been detached from
+ * the UI with {@link #detach(Fragment)}. This
+ * causes its view hierarchy to be re-created, attached to the UI,
+ * and displayed.
+ *
+ * @param fragment The fragment to be attached.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public abstract FragmentTransaction attach(Fragment fragment);
+
+ /**
+ * Set a currently active fragment in this FragmentManager as the primary navigation fragment.
+ *
+ * <p>The primary navigation fragment's
+ * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first
+ * to process delegated navigation actions such as {@link FragmentManager#popBackStack()}
+ * if no ID or transaction name is provided to pop to. Navigation operations outside of the
+ * fragment system may choose to delegate those actions to the primary navigation fragment
+ * as returned by {@link FragmentManager#getPrimaryNavigationFragment()}.</p>
+ *
+ * <p>The fragment provided must currently be added to the FragmentManager to be set as
+ * a primary navigation fragment, or previously added as part of this transaction.</p>
+ *
+ * @param fragment the fragment to set as the primary navigation fragment
+ * @return the same FragmentTransaction instance
+ */
+ public abstract FragmentTransaction setPrimaryNavigationFragment(Fragment fragment);
+
+ /**
+ * @return <code>true</code> if this transaction contains no operations,
+ * <code>false</code> otherwise.
+ */
+ public abstract boolean isEmpty();
+
+ /**
+ * Bit mask that is set for all enter transitions.
+ */
+ public static final int TRANSIT_ENTER_MASK = 0x1000;
+
+ /**
+ * Bit mask that is set for all exit transitions.
+ */
+ public static final int TRANSIT_EXIT_MASK = 0x2000;
+
+ /** Not set up for a transition. */
+ public static final int TRANSIT_UNSET = -1;
+ /** No animation for transition. */
+ public static final int TRANSIT_NONE = 0;
+ /** Fragment is being added onto the stack */
+ public static final int TRANSIT_FRAGMENT_OPEN = 1 | TRANSIT_ENTER_MASK;
+ /** Fragment is being removed from the stack */
+ public static final int TRANSIT_FRAGMENT_CLOSE = 2 | TRANSIT_EXIT_MASK;
+ /** Fragment should simply fade in or out; that is, no strong navigation associated
+ * with it except that it is appearing or disappearing for some reason. */
+ public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK;
+
+ /** @hide */
+ @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Transit {}
+
+ /**
+ * Set specific animation resources to run for the fragments that are
+ * entering and exiting in this transaction. These animations will not be
+ * played when popping the back stack.
+ */
+ public abstract FragmentTransaction setCustomAnimations(@AnimatorRes int enter,
+ @AnimatorRes int exit);
+
+ /**
+ * Set specific animation resources to run for the fragments that are
+ * entering and exiting in this transaction. The <code>popEnter</code>
+ * and <code>popExit</code> animations will be played for enter/exit
+ * operations specifically when popping the back stack.
+ */
+ public abstract FragmentTransaction setCustomAnimations(@AnimatorRes int enter,
+ @AnimatorRes int exit, @AnimatorRes int popEnter, @AnimatorRes int popExit);
+
+ /**
+ * Select a standard transition animation for this transaction. May be
+ * one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN},
+ * {@link #TRANSIT_FRAGMENT_CLOSE}, or {@link #TRANSIT_FRAGMENT_FADE}.
+ */
+ public abstract FragmentTransaction setTransition(@Transit int transit);
+
+ /**
+ * Used with to map a View from a removed or hidden Fragment to a View from a shown
+ * or added Fragment.
+ * @param sharedElement A View in a disappearing Fragment to match with a View in an
+ * appearing Fragment.
+ * @param name The transitionName for a View in an appearing Fragment to match to the shared
+ * element.
+ */
+ public abstract FragmentTransaction addSharedElement(View sharedElement, String name);
+
+ /**
+ * Set a custom style resource that will be used for resolving transit
+ * animations.
+ */
+ public abstract FragmentTransaction setTransitionStyle(@StyleRes int styleRes);
+
+ /**
+ * Add this transaction to the back stack. This means that the transaction
+ * will be remembered after it is committed, and will reverse its operation
+ * when later popped off the stack.
+ *
+ * @param name An optional name for this back stack state, or null.
+ */
+ public abstract FragmentTransaction addToBackStack(@Nullable String name);
+
+ /**
+ * Returns true if this FragmentTransaction is allowed to be added to the back
+ * stack. If this method would return false, {@link #addToBackStack(String)}
+ * will throw {@link IllegalStateException}.
+ *
+ * @return True if {@link #addToBackStack(String)} is permitted on this transaction.
+ */
+ public abstract boolean isAddToBackStackAllowed();
+
+ /**
+ * Disallow calls to {@link #addToBackStack(String)}. Any future calls to
+ * addToBackStack will throw {@link IllegalStateException}. If addToBackStack
+ * has already been called, this method will throw IllegalStateException.
+ */
+ public abstract FragmentTransaction disallowAddToBackStack();
+
+ /**
+ * Set the full title to show as a bread crumb when this transaction
+ * is on the back stack, as used by {@link FragmentBreadCrumbs}.
+ *
+ * @param res A string resource containing the title.
+ */
+ public abstract FragmentTransaction setBreadCrumbTitle(@StringRes int res);
+
+ /**
+ * Like {@link #setBreadCrumbTitle(int)} but taking a raw string; this
+ * method is <em>not</em> recommended, as the string can not be changed
+ * later if the locale changes.
+ */
+ public abstract FragmentTransaction setBreadCrumbTitle(CharSequence text);
+
+ /**
+ * Set the short title to show as a bread crumb when this transaction
+ * is on the back stack, as used by {@link FragmentBreadCrumbs}.
+ *
+ * @param res A string resource containing the title.
+ */
+ public abstract FragmentTransaction setBreadCrumbShortTitle(@StringRes int res);
+
+ /**
+ * Like {@link #setBreadCrumbShortTitle(int)} but taking a raw string; this
+ * method is <em>not</em> recommended, as the string can not be changed
+ * later if the locale changes.
+ */
+ public abstract FragmentTransaction setBreadCrumbShortTitle(CharSequence text);
+
+ /**
+ * Sets whether or not to allow optimizing operations within and across
+ * transactions. This will remove redundant operations, eliminating
+ * operations that cancel. For example, if two transactions are executed
+ * together, one that adds a fragment A and the next replaces it with fragment B,
+ * the operations will cancel and only fragment B will be added. That means that
+ * fragment A may not go through the creation/destruction lifecycle.
+ * <p>
+ * The side effect of removing redundant operations is that fragments may have state changes
+ * out of the expected order. For example, one transaction adds fragment A,
+ * a second adds fragment B, then a third removes fragment A. Without removing the redundant
+ * operations, fragment B could expect that while it is being created, fragment A will also
+ * exist because fragment A will be removed after fragment B was added.
+ * With removing redundant operations, fragment B cannot expect fragment A to exist when
+ * it has been created because fragment A's add/remove will be optimized out.
+ * <p>
+ * It can also reorder the state changes of Fragments to allow for better Transitions.
+ * Added Fragments may have {@link Fragment#onCreate(Bundle)} called before replaced
+ * Fragments have {@link Fragment#onDestroy()} called.
+ * <p>
+ * The default is {@code false} for applications targeting version
+ * versions prior to O and {@code true} for applications targeting O and
+ * later.
+ *
+ * @param reorderingAllowed {@code true} to enable optimizing out redundant operations
+ * or {@code false} to disable optimizing out redundant
+ * operations on this transaction.
+ */
+ public abstract FragmentTransaction setReorderingAllowed(boolean reorderingAllowed);
+
+ /**
+ * Add a Runnable to this transaction that will be run after this transaction has
+ * been committed. If fragment transactions are {@link #setReorderingAllowed(boolean) optimized}
+ * this may be after other subsequent fragment operations have also taken place, or operations
+ * in this transaction may have been optimized out due to the presence of a subsequent
+ * fragment transaction in the batch.
+ *
+ *
+ * <p>If a transaction is committed using {@link #commitAllowingStateLoss()} this runnable
+ * may be executed when the FragmentManager is in a state where new transactions may not
+ * be committed without allowing state loss.</p>
+ *
+ * <p><code>runOnCommit</code> may not be used with transactions
+ * {@link #addToBackStack(String) added to the back stack} as Runnables cannot be persisted
+ * with back stack state. {@link IllegalStateException} will be thrown if
+ * {@link #addToBackStack(String)} has been previously called for this transaction
+ * or if it is called after a call to <code>runOnCommit</code>.</p>
+ *
+ * @param runnable Runnable to add
+ * @return this FragmentTransaction
+ * @throws IllegalStateException if {@link #addToBackStack(String)} has been called
+ */
+ public abstract FragmentTransaction runOnCommit(Runnable runnable);
+
+ /**
+ * Schedules a commit of this transaction. The commit does
+ * not happen immediately; it will be scheduled as work on the main thread
+ * to be done the next time that thread is ready.
+ *
+ * <p class="note">A transaction can only be committed with this method
+ * prior to its containing activity saving its state. If the commit is
+ * attempted after that point, an exception will be thrown. This is
+ * because the state after the commit can be lost if the activity needs to
+ * be restored from its state. See {@link #commitAllowingStateLoss()} for
+ * situations where it may be okay to lose the commit.</p>
+ *
+ * @return Returns the identifier of this transaction's back stack entry,
+ * if {@link #addToBackStack(String)} had been called. Otherwise, returns
+ * a negative number.
+ */
+ public abstract int commit();
+
+ /**
+ * Like {@link #commit} but allows the commit to be executed after an
+ * activity's state is saved. This is dangerous because the commit can
+ * be lost if the activity needs to later be restored from its state, so
+ * this should only be used for cases where it is okay for the UI state
+ * to change unexpectedly on the user.
+ */
+ public abstract int commitAllowingStateLoss();
+
+ /**
+ * Commits this transaction synchronously. Any added fragments will be
+ * initialized and brought completely to the lifecycle state of their host
+ * and any removed fragments will be torn down accordingly before this
+ * call returns. Committing a transaction in this way allows fragments
+ * to be added as dedicated, encapsulated components that monitor the
+ * lifecycle state of their host while providing firmer ordering guarantees
+ * around when those fragments are fully initialized and ready. Fragments
+ * that manage views will have those views created and attached.
+ *
+ * <p>Calling <code>commitNow</code> is preferable to calling
+ * {@link #commit()} followed by {@link FragmentManager#executePendingTransactions()}
+ * as the latter will have the side effect of attempting to commit <em>all</em>
+ * currently pending transactions whether that is the desired behavior
+ * or not.</p>
+ *
+ * <p>Transactions committed in this way may not be added to the
+ * FragmentManager's back stack, as doing so would break other expected
+ * ordering guarantees for other asynchronously committed transactions.
+ * This method will throw {@link IllegalStateException} if the transaction
+ * previously requested to be added to the back stack with
+ * {@link #addToBackStack(String)}.</p>
+ *
+ * <p class="note">A transaction can only be committed with this method
+ * prior to its containing activity saving its state. If the commit is
+ * attempted after that point, an exception will be thrown. This is
+ * because the state after the commit can be lost if the activity needs to
+ * be restored from its state. See {@link #commitAllowingStateLoss()} for
+ * situations where it may be okay to lose the commit.</p>
+ */
+ public abstract void commitNow();
+
+ /**
+ * Like {@link #commitNow} but allows the commit to be executed after an
+ * activity's state is saved. This is dangerous because the commit can
+ * be lost if the activity needs to later be restored from its state, so
+ * this should only be used for cases where it is okay for the UI state
+ * to change unexpectedly on the user.
+ */
+ public abstract void commitNowAllowingStateLoss();
+}
diff --git a/android/app/FragmentTransition.java b/android/app/FragmentTransition.java
new file mode 100644
index 00000000..ceb828b6
--- /dev/null
+++ b/android/app/FragmentTransition.java
@@ -0,0 +1,1386 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.transition.Transition;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.view.OneShotPreDrawListener;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains the Fragment Transition functionality for both ordered and reordered
+ * Fragment Transactions. With reordered fragment transactions, all Views have been
+ * added to the View hierarchy prior to calling startTransitions. With ordered
+ * fragment transactions, Views will be removed and added after calling startTransitions.
+ */
+class FragmentTransition {
+ /**
+ * The inverse of all BackStackRecord operation commands. This assumes that
+ * REPLACE operations have already been replaced by add/remove operations.
+ */
+ private static final int[] INVERSE_OPS = {
+ BackStackRecord.OP_NULL, // inverse of OP_NULL (error)
+ BackStackRecord.OP_REMOVE, // inverse of OP_ADD
+ BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error)
+ BackStackRecord.OP_ADD, // inverse of OP_REMOVE
+ BackStackRecord.OP_SHOW, // inverse of OP_HIDE
+ BackStackRecord.OP_HIDE, // inverse of OP_SHOW
+ BackStackRecord.OP_ATTACH, // inverse of OP_DETACH
+ BackStackRecord.OP_DETACH, // inverse of OP_ATTACH
+ BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
+ BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV
+ };
+
+ /**
+ * The main entry point for Fragment Transitions, this starts the transitions
+ * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the
+ * entering Fragment's {@link Fragment#getEnterTransition()} and
+ * {@link Fragment#getSharedElementEnterTransition()}. When popping,
+ * the leaving Fragment's {@link Fragment#getReturnTransition()} and
+ * {@link Fragment#getSharedElementReturnTransition()} and the entering
+ * {@link Fragment#getReenterTransition()} will be run.
+ * <p>
+ * With reordered Fragment Transitions, all Views have been added to the
+ * View hierarchy prior to calling this method. The incoming Fragment's Views
+ * will be INVISIBLE. With ordered Fragment Transitions, this method
+ * is called before any change has been made to the hierarchy. That means
+ * that the added Fragments have not created their Views yet and the hierarchy
+ * is unknown.
+ *
+ * @param fragmentManager The executing FragmentManagerImpl
+ * @param records The list of transactions being executed.
+ * @param isRecordPop For each transaction, whether it is a pop transaction or not.
+ * @param startIndex The first index into records and isRecordPop to execute as
+ * part of this transition.
+ * @param endIndex One past the last index into records and isRecordPop to execute
+ * as part of this transition.
+ * @param isReordered true if this is a reordered transaction, meaning that the
+ * Views of incoming fragments have been added. false if the
+ * transaction has yet to be run and Views haven't been created.
+ */
+ static void startTransitions(FragmentManagerImpl fragmentManager,
+ ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
+ int startIndex, int endIndex, boolean isReordered) {
+ if (fragmentManager.mCurState < Fragment.CREATED) {
+ return;
+ }
+ SparseArray<FragmentContainerTransition> transitioningFragments =
+ new SparseArray<>();
+ for (int i = startIndex; i < endIndex; i++) {
+ final BackStackRecord record = records.get(i);
+ final boolean isPop = isRecordPop.get(i);
+ if (isPop) {
+ calculatePopFragments(record, transitioningFragments, isReordered);
+ } else {
+ calculateFragments(record, transitioningFragments, isReordered);
+ }
+ }
+
+ if (transitioningFragments.size() != 0) {
+ final View nonExistentView = new View(fragmentManager.mHost.getContext());
+ final int numContainers = transitioningFragments.size();
+ for (int i = 0; i < numContainers; i++) {
+ int containerId = transitioningFragments.keyAt(i);
+ ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,
+ records, isRecordPop, startIndex, endIndex);
+
+ FragmentContainerTransition containerTransition = transitioningFragments.valueAt(i);
+
+ if (isReordered) {
+ configureTransitionsReordered(fragmentManager, containerId,
+ containerTransition, nonExistentView, nameOverrides);
+ } else {
+ configureTransitionsOrdered(fragmentManager, containerId,
+ containerTransition, nonExistentView, nameOverrides);
+ }
+ }
+ }
+ }
+
+ /**
+ * Iterates through the transactions that affect a given fragment container
+ * and tracks the shared element names across transactions. This is most useful
+ * in pop transactions where the names of shared elements are known.
+ *
+ * @param containerId The container ID that is executing the transition.
+ * @param records The list of transactions being executed.
+ * @param isRecordPop For each transaction, whether it is a pop transaction or not.
+ * @param startIndex The first index into records and isRecordPop to execute as
+ * part of this transition.
+ * @param endIndex One past the last index into records and isRecordPop to execute
+ * as part of this transition.
+ * @return A map from the initial shared element name to the final shared element name
+ * before any onMapSharedElements is run.
+ */
+ private static ArrayMap<String, String> calculateNameOverrides(int containerId,
+ ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
+ int startIndex, int endIndex) {
+ ArrayMap<String, String> nameOverrides = new ArrayMap<>();
+ for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) {
+ final BackStackRecord record = records.get(recordNum);
+ if (!record.interactsWith(containerId)) {
+ continue;
+ }
+ final boolean isPop = isRecordPop.get(recordNum);
+ if (record.mSharedElementSourceNames != null) {
+ final int numSharedElements = record.mSharedElementSourceNames.size();
+ final ArrayList<String> sources;
+ final ArrayList<String> targets;
+ if (isPop) {
+ targets = record.mSharedElementSourceNames;
+ sources = record.mSharedElementTargetNames;
+ } else {
+ sources = record.mSharedElementSourceNames;
+ targets = record.mSharedElementTargetNames;
+ }
+ for (int i = 0; i < numSharedElements; i++) {
+ String sourceName = sources.get(i);
+ String targetName = targets.get(i);
+ String previousTarget = nameOverrides.remove(targetName);
+ if (previousTarget != null) {
+ nameOverrides.put(sourceName, previousTarget);
+ } else {
+ nameOverrides.put(sourceName, targetName);
+ }
+ }
+ }
+ }
+ return nameOverrides;
+ }
+
+ /**
+ * Configures a transition for a single fragment container for which the transaction was
+ * reordered. That means that all Fragment Views have been added and incoming fragment
+ * Views are marked invisible.
+ *
+ * @param fragmentManager The executing FragmentManagerImpl
+ * @param containerId The container ID that is executing the transition.
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @param nonExistentView A View that does not exist in the hierarchy. This is used to
+ * prevent transitions from acting on other Views when there is no
+ * other target.
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ */
+ private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,
+ int containerId, FragmentContainerTransition fragments,
+ View nonExistentView, ArrayMap<String, String> nameOverrides) {
+ ViewGroup sceneRoot = null;
+ if (fragmentManager.mContainer.onHasView()) {
+ sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
+ }
+ if (sceneRoot == null) {
+ return;
+ }
+ final Fragment inFragment = fragments.lastIn;
+ final Fragment outFragment = fragments.firstOut;
+ final boolean inIsPop = fragments.lastInIsPop;
+ final boolean outIsPop = fragments.firstOutIsPop;
+
+ ArrayList<View> sharedElementsIn = new ArrayList<>();
+ ArrayList<View> sharedElementsOut = new ArrayList<>();
+ Transition enterTransition = getEnterTransition(inFragment, inIsPop);
+ Transition exitTransition = getExitTransition(outFragment, outIsPop);
+
+ TransitionSet sharedElementTransition = configureSharedElementsReordered(sceneRoot,
+ nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
+ enterTransition, exitTransition);
+
+ if (enterTransition == null && sharedElementTransition == null &&
+ exitTransition == null) {
+ return; // no transitions!
+ }
+
+ ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
+ outFragment, sharedElementsOut, nonExistentView);
+
+ ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition,
+ inFragment, sharedElementsIn, nonExistentView);
+
+ setViewVisibility(enteringViews, View.INVISIBLE);
+
+ Transition transition = mergeTransitions(enterTransition, exitTransition,
+ sharedElementTransition, inFragment, inIsPop);
+
+ if (transition != null) {
+ replaceHide(exitTransition, outFragment, exitingViews);
+ transition.setNameOverrides(nameOverrides);
+ scheduleRemoveTargets(transition,
+ enterTransition, enteringViews, exitTransition, exitingViews,
+ sharedElementTransition, sharedElementsIn);
+ TransitionManager.beginDelayedTransition(sceneRoot, transition);
+ setViewVisibility(enteringViews, View.VISIBLE);
+ // Swap the shared element targets
+ if (sharedElementTransition != null) {
+ sharedElementTransition.getTargets().clear();
+ sharedElementTransition.getTargets().addAll(sharedElementsIn);
+ replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
+ }
+ }
+ }
+
+ /**
+ * Configures a transition for a single fragment container for which the transaction was
+ * ordered. That means that the transaction has not been executed yet, so incoming
+ * Views are not yet known.
+ *
+ * @param fragmentManager The executing FragmentManagerImpl
+ * @param containerId The container ID that is executing the transition.
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @param nonExistentView A View that does not exist in the hierarchy. This is used to
+ * prevent transitions from acting on other Views when there is no
+ * other target.
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ */
+ private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,
+ int containerId, FragmentContainerTransition fragments,
+ View nonExistentView, ArrayMap<String, String> nameOverrides) {
+ ViewGroup sceneRoot = null;
+ if (fragmentManager.mContainer.onHasView()) {
+ sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
+ }
+ if (sceneRoot == null) {
+ return;
+ }
+ final Fragment inFragment = fragments.lastIn;
+ final Fragment outFragment = fragments.firstOut;
+ final boolean inIsPop = fragments.lastInIsPop;
+ final boolean outIsPop = fragments.firstOutIsPop;
+
+ Transition enterTransition = getEnterTransition(inFragment, inIsPop);
+ Transition exitTransition = getExitTransition(outFragment, outIsPop);
+
+ ArrayList<View> sharedElementsOut = new ArrayList<>();
+ ArrayList<View> sharedElementsIn = new ArrayList<>();
+
+ TransitionSet sharedElementTransition = configureSharedElementsOrdered(sceneRoot,
+ nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
+ enterTransition, exitTransition);
+
+ if (enterTransition == null && sharedElementTransition == null &&
+ exitTransition == null) {
+ return; // no transitions!
+ }
+
+ ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
+ outFragment, sharedElementsOut, nonExistentView);
+
+ if (exitingViews == null || exitingViews.isEmpty()) {
+ exitTransition = null;
+ }
+
+ if (enterTransition != null) {
+ // Ensure the entering transition doesn't target anything until the views are made
+ // visible
+ enterTransition.addTarget(nonExistentView);
+ }
+
+ Transition transition = mergeTransitions(enterTransition, exitTransition,
+ sharedElementTransition, inFragment, fragments.lastInIsPop);
+
+ if (transition != null) {
+ transition.setNameOverrides(nameOverrides);
+ final ArrayList<View> enteringViews = new ArrayList<>();
+ scheduleRemoveTargets(transition,
+ enterTransition, enteringViews, exitTransition, exitingViews,
+ sharedElementTransition, sharedElementsIn);
+ scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn,
+ enterTransition, enteringViews, exitTransition, exitingViews);
+
+ TransitionManager.beginDelayedTransition(sceneRoot, transition);
+ }
+ }
+
+ /**
+ * Replace hide operations with visibility changes on the exiting views. Instead of making
+ * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the
+ * transition, make the fragment's view GONE.
+ */
+ private static void replaceHide(Transition exitTransition, Fragment exitingFragment,
+ final ArrayList<View> exitingViews) {
+ if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded
+ && exitingFragment.mHidden && exitingFragment.mHiddenChanged) {
+ exitingFragment.setHideReplaced(true);
+ final View fragmentView = exitingFragment.getView();
+ OneShotPreDrawListener.add(exitingFragment.mContainer, () -> {
+ setViewVisibility(exitingViews, View.INVISIBLE);
+ });
+ exitTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ fragmentView.setVisibility(View.GONE);
+ setViewVisibility(exitingViews, View.VISIBLE);
+ }
+ });
+ }
+ }
+
+ /**
+ * This method is used for fragment transitions for ordered transactions to change the
+ * enter and exit transition targets after the call to
+ * {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. The exit transition
+ * must ensure that it does not target any Views and the enter transition must start targeting
+ * the Views of the incoming Fragment.
+ *
+ * @param sceneRoot The fragment container View
+ * @param inFragment The last fragment that is entering
+ * @param nonExistentView A view that does not exist in the hierarchy that is used as a
+ * transition target to ensure no View is targeted.
+ * @param sharedElementsIn The shared element Views of the incoming fragment
+ * @param enterTransition The enter transition of the incoming fragment
+ * @param enteringViews The entering Views of the incoming fragment
+ * @param exitTransition The exit transition of the outgoing fragment
+ * @param exitingViews The exiting views of the outgoing fragment
+ */
+ private static void scheduleTargetChange(final ViewGroup sceneRoot,
+ final Fragment inFragment, final View nonExistentView,
+ final ArrayList<View> sharedElementsIn,
+ final Transition enterTransition, final ArrayList<View> enteringViews,
+ final Transition exitTransition, final ArrayList<View> exitingViews) {
+
+ OneShotPreDrawListener.add(sceneRoot, () -> {
+ if (enterTransition != null) {
+ enterTransition.removeTarget(nonExistentView);
+ ArrayList<View> views = configureEnteringExitingViews(
+ enterTransition, inFragment, sharedElementsIn, nonExistentView);
+ enteringViews.addAll(views);
+ }
+
+ if (exitingViews != null) {
+ if (exitTransition != null) {
+ ArrayList<View> tempExiting = new ArrayList<>();
+ tempExiting.add(nonExistentView);
+ replaceTargets(exitTransition, exitingViews, tempExiting);
+ }
+ exitingViews.clear();
+ exitingViews.add(nonExistentView);
+ }
+ });
+ }
+
+ /**
+ * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet
+ * targets all shared elements to ensure that no other Views are targeted. The shared element
+ * transition can then target any or all shared elements without worrying about accidentally
+ * targeting entering or exiting Views.
+ *
+ * @param inFragment The incoming fragment
+ * @param outFragment the outgoing fragment
+ * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.
+ * @return A TransitionSet wrapping the shared element transition or null if no such transition
+ * exists.
+ */
+ private static TransitionSet getSharedElementTransition(Fragment inFragment,
+ Fragment outFragment, boolean isPop) {
+ if (inFragment == null || outFragment == null) {
+ return null;
+ }
+ Transition transition = cloneTransition(isPop
+ ? outFragment.getSharedElementReturnTransition()
+ : inFragment.getSharedElementEnterTransition());
+ if (transition == null) {
+ return null;
+ }
+ TransitionSet transitionSet = new TransitionSet();
+ transitionSet.addTransition(transition);
+ return transitionSet;
+ }
+
+ /**
+ * Returns a clone of the enter transition or null if no such transition exists.
+ */
+ private static Transition getEnterTransition(Fragment inFragment, boolean isPop) {
+ if (inFragment == null) {
+ return null;
+ }
+ return cloneTransition(isPop ? inFragment.getReenterTransition() :
+ inFragment.getEnterTransition());
+ }
+
+ /**
+ * Returns a clone of the exit transition or null if no such transition exists.
+ */
+ private static Transition getExitTransition(Fragment outFragment, boolean isPop) {
+ if (outFragment == null) {
+ return null;
+ }
+ return cloneTransition(isPop ? outFragment.getReturnTransition() :
+ outFragment.getExitTransition());
+ }
+
+ /**
+ * Returns a clone of a transition or null if it is null
+ */
+ private static Transition cloneTransition(Transition transition) {
+ if (transition != null) {
+ transition = transition.clone();
+ }
+ return transition;
+ }
+
+ /**
+ * Configures the shared elements of an reordered fragment transaction's transition.
+ * This retrieves the shared elements of the outgoing and incoming fragments, maps the
+ * views, and sets up the epicenter on the transitions.
+ * <p>
+ * The epicenter of exit and shared element transitions is the first shared element
+ * in the outgoing fragment. The epicenter of the entering transition is the first shared
+ * element in the incoming fragment.
+ *
+ * @param sceneRoot The fragment container View
+ * @param nonExistentView A View that does not exist in the hierarchy. This is used to
+ * prevent transitions from acting on other Views when there is no
+ * other target.
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
+ * fragment
+ * @param sharedElementsIn A list modified to contain the shared elements in the incoming
+ * fragment
+ * @param enterTransition The transition used for entering Views, modified by applying the
+ * epicenter
+ * @param exitTransition The transition used for exiting Views, modified by applying the
+ * epicenter
+ * @return The shared element transition or null if no shared elements exist
+ */
+ private static TransitionSet configureSharedElementsReordered(final ViewGroup sceneRoot,
+ final View nonExistentView, ArrayMap<String, String> nameOverrides,
+ final FragmentContainerTransition fragments,
+ final ArrayList<View> sharedElementsOut,
+ final ArrayList<View> sharedElementsIn,
+ final Transition enterTransition, final Transition exitTransition) {
+ final Fragment inFragment = fragments.lastIn;
+ final Fragment outFragment = fragments.firstOut;
+ if (inFragment != null) {
+ inFragment.getView().setVisibility(View.VISIBLE);
+ }
+ if (inFragment == null || outFragment == null) {
+ return null; // no shared element without a fragment
+ }
+
+ final boolean inIsPop = fragments.lastInIsPop;
+ TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
+ : getSharedElementTransition(inFragment, outFragment, inIsPop);
+
+ ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
+ sharedElementTransition, fragments);
+
+ ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides,
+ sharedElementTransition, fragments);
+
+ if (nameOverrides.isEmpty()) {
+ sharedElementTransition = null;
+ if (outSharedElements != null) {
+ outSharedElements.clear();
+ }
+ if (inSharedElements != null) {
+ inSharedElements.clear();
+ }
+ } else {
+ addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,
+ nameOverrides.keySet());
+ addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,
+ nameOverrides.values());
+ }
+
+ if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
+ // don't call onSharedElementStart/End since there is no transition
+ return null;
+ }
+
+ callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
+
+ final Rect epicenter;
+ final View epicenterView;
+ if (sharedElementTransition != null) {
+ sharedElementsIn.add(nonExistentView);
+ setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
+ final boolean outIsPop = fragments.firstOutIsPop;
+ final BackStackRecord outTransaction = fragments.firstOutTransaction;
+ setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
+ outTransaction);
+ epicenter = new Rect();
+ epicenterView = getInEpicenterView(inSharedElements, fragments,
+ enterTransition, inIsPop);
+ if (epicenterView != null) {
+ enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+ }
+ } else {
+ epicenter = null;
+ epicenterView = null;
+ }
+
+ OneShotPreDrawListener.add(sceneRoot, () -> {
+ callSharedElementStartEnd(inFragment, outFragment, inIsPop,
+ inSharedElements, false);
+ if (epicenterView != null) {
+ epicenterView.getBoundsOnScreen(epicenter);
+ }
+ });
+ return sharedElementTransition;
+ }
+
+ /**
+ * Add Views from sharedElements into views that have the transitionName in the
+ * nameOverridesSet.
+ *
+ * @param views Views list to add shared elements to
+ * @param sharedElements List of shared elements
+ * @param nameOverridesSet The transition names for all views to be copied from
+ * sharedElements to views.
+ */
+ private static void addSharedElementsWithMatchingNames(ArrayList<View> views,
+ ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {
+ for (int i = sharedElements.size() - 1; i >= 0; i--) {
+ View view = sharedElements.valueAt(i);
+ if (view != null && nameOverridesSet.contains(view.getTransitionName())) {
+ views.add(view);
+ }
+ }
+ }
+
+ /**
+ * Configures the shared elements of an ordered fragment transaction's transition.
+ * This retrieves the shared elements of the incoming fragments, and schedules capturing
+ * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter
+ * on the transitions.
+ * <p>
+ * The epicenter of exit and shared element transitions is the first shared element
+ * in the outgoing fragment. The epicenter of the entering transition is the first shared
+ * element in the incoming fragment.
+ *
+ * @param sceneRoot The fragment container View
+ * @param nonExistentView A View that does not exist in the hierarchy. This is used to
+ * prevent transitions from acting on other Views when there is no
+ * other target.
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
+ * fragment
+ * @param sharedElementsIn A list modified to contain the shared elements in the incoming
+ * fragment
+ * @param enterTransition The transition used for entering Views, modified by applying the
+ * epicenter
+ * @param exitTransition The transition used for exiting Views, modified by applying the
+ * epicenter
+ * @return The shared element transition or null if no shared elements exist
+ */
+ private static TransitionSet configureSharedElementsOrdered(final ViewGroup sceneRoot,
+ final View nonExistentView, ArrayMap<String, String> nameOverrides,
+ final FragmentContainerTransition fragments,
+ final ArrayList<View> sharedElementsOut,
+ final ArrayList<View> sharedElementsIn,
+ final Transition enterTransition, final Transition exitTransition) {
+ final Fragment inFragment = fragments.lastIn;
+ final Fragment outFragment = fragments.firstOut;
+
+ if (inFragment == null || outFragment == null) {
+ return null; // no transition
+ }
+
+ final boolean inIsPop = fragments.lastInIsPop;
+ TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
+ : getSharedElementTransition(inFragment, outFragment, inIsPop);
+
+ ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
+ sharedElementTransition, fragments);
+
+ if (nameOverrides.isEmpty()) {
+ sharedElementTransition = null;
+ } else {
+ sharedElementsOut.addAll(outSharedElements.values());
+ }
+
+ if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
+ // don't call onSharedElementStart/End since there is no transition
+ return null;
+ }
+
+ callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
+
+ final Rect inEpicenter;
+ if (sharedElementTransition != null) {
+ inEpicenter = new Rect();
+ setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
+ final boolean outIsPop = fragments.firstOutIsPop;
+ final BackStackRecord outTransaction = fragments.firstOutTransaction;
+ setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
+ outTransaction);
+ if (enterTransition != null) {
+ enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ if (inEpicenter.isEmpty()) {
+ return null;
+ }
+ return inEpicenter;
+ }
+ });
+ }
+ } else {
+ inEpicenter = null;
+ }
+
+ TransitionSet finalSharedElementTransition = sharedElementTransition;
+
+ OneShotPreDrawListener.add(sceneRoot, () -> {
+ ArrayMap<String, View> inSharedElements = captureInSharedElements(
+ nameOverrides, finalSharedElementTransition, fragments);
+
+ if (inSharedElements != null) {
+ sharedElementsIn.addAll(inSharedElements.values());
+ sharedElementsIn.add(nonExistentView);
+ }
+
+ callSharedElementStartEnd(inFragment, outFragment, inIsPop,
+ inSharedElements, false);
+ if (finalSharedElementTransition != null) {
+ finalSharedElementTransition.getTargets().clear();
+ finalSharedElementTransition.getTargets().addAll(sharedElementsIn);
+ replaceTargets(finalSharedElementTransition, sharedElementsOut,
+ sharedElementsIn);
+
+ final View inEpicenterView = getInEpicenterView(inSharedElements,
+ fragments, enterTransition, inIsPop);
+ if (inEpicenterView != null) {
+ inEpicenterView.getBoundsOnScreen(inEpicenter);
+ }
+ }
+ });
+ return sharedElementTransition;
+ }
+
+ /**
+ * Finds the shared elements in the outgoing fragment. It also calls
+ * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
+ * of the shared element mapping. {@code nameOverrides} is updated to match the
+ * actual transition name of the mapped shared elements.
+ *
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ * @param sharedElementTransition The shared element transition
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @return The mapping of shared element names to the Views in the hierarchy or null
+ * if there is no shared element transition.
+ */
+ private static ArrayMap<String, View> captureOutSharedElements(
+ ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
+ FragmentContainerTransition fragments) {
+ if (nameOverrides.isEmpty() || sharedElementTransition == null) {
+ nameOverrides.clear();
+ return null;
+ }
+ final Fragment outFragment = fragments.firstOut;
+ final ArrayMap<String, View> outSharedElements = new ArrayMap<>();
+ outFragment.getView().findNamedViews(outSharedElements);
+
+ final SharedElementCallback sharedElementCallback;
+ final ArrayList<String> names;
+ final BackStackRecord outTransaction = fragments.firstOutTransaction;
+ if (fragments.firstOutIsPop) {
+ sharedElementCallback = outFragment.getEnterTransitionCallback();
+ names = outTransaction.mSharedElementTargetNames;
+ } else {
+ sharedElementCallback = outFragment.getExitTransitionCallback();
+ names = outTransaction.mSharedElementSourceNames;
+ }
+
+ outSharedElements.retainAll(names);
+ if (sharedElementCallback != null) {
+ sharedElementCallback.onMapSharedElements(names, outSharedElements);
+ for (int i = names.size() - 1; i >= 0; i--) {
+ String name = names.get(i);
+ View view = outSharedElements.get(name);
+ if (view == null) {
+ nameOverrides.remove(name);
+ } else if (!name.equals(view.getTransitionName())) {
+ String targetValue = nameOverrides.remove(name);
+ nameOverrides.put(view.getTransitionName(), targetValue);
+ }
+ }
+ } else {
+ nameOverrides.retainAll(outSharedElements.keySet());
+ }
+ return outSharedElements;
+ }
+
+ /**
+ * Finds the shared elements in the incoming fragment. It also calls
+ * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
+ * of the shared element mapping. {@code nameOverrides} is updated to match the
+ * actual transition name of the mapped shared elements.
+ *
+ * @param nameOverrides A map of the shared element names from the starting fragment to
+ * the final fragment's Views as given in
+ * {@link FragmentTransaction#addSharedElement(View, String)}.
+ * @param sharedElementTransition The shared element transition
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @return The mapping of shared element names to the Views in the hierarchy or null
+ * if there is no shared element transition.
+ */
+ private static ArrayMap<String, View> captureInSharedElements(
+ ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
+ FragmentContainerTransition fragments) {
+ Fragment inFragment = fragments.lastIn;
+ final View fragmentView = inFragment.getView();
+ if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {
+ nameOverrides.clear();
+ return null;
+ }
+ final ArrayMap<String, View> inSharedElements = new ArrayMap<>();
+ fragmentView.findNamedViews(inSharedElements);
+
+ final SharedElementCallback sharedElementCallback;
+ final ArrayList<String> names;
+ final BackStackRecord inTransaction = fragments.lastInTransaction;
+ if (fragments.lastInIsPop) {
+ sharedElementCallback = inFragment.getExitTransitionCallback();
+ names = inTransaction.mSharedElementSourceNames;
+ } else {
+ sharedElementCallback = inFragment.getEnterTransitionCallback();
+ names = inTransaction.mSharedElementTargetNames;
+ }
+
+ if (names != null) {
+ inSharedElements.retainAll(names);
+ }
+ if (names != null && sharedElementCallback != null) {
+ sharedElementCallback.onMapSharedElements(names, inSharedElements);
+ for (int i = names.size() - 1; i >= 0; i--) {
+ String name = names.get(i);
+ View view = inSharedElements.get(name);
+ if (view == null) {
+ String key = findKeyForValue(nameOverrides, name);
+ if (key != null) {
+ nameOverrides.remove(key);
+ }
+ } else if (!name.equals(view.getTransitionName())) {
+ String key = findKeyForValue(nameOverrides, name);
+ if (key != null) {
+ nameOverrides.put(key, view.getTransitionName());
+ }
+ }
+ }
+ } else {
+ retainValues(nameOverrides, inSharedElements);
+ }
+ return inSharedElements;
+ }
+
+ /**
+ * Utility to find the String key in {@code map} that maps to {@code value}.
+ */
+ private static String findKeyForValue(ArrayMap<String, String> map, String value) {
+ final int numElements = map.size();
+ for (int i = 0; i < numElements; i++) {
+ if (value.equals(map.valueAt(i))) {
+ return map.keyAt(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the View in the incoming Fragment that should be used as the epicenter.
+ *
+ * @param inSharedElements The mapping of shared element names to Views in the
+ * incoming fragment.
+ * @param fragments A structure holding the transitioning fragments in this container.
+ * @param enterTransition The transition used for the incoming Fragment's views
+ * @param inIsPop Is the incoming fragment being added as a pop transaction?
+ */
+ private static View getInEpicenterView(ArrayMap<String, View> inSharedElements,
+ FragmentContainerTransition fragments,
+ Transition enterTransition, boolean inIsPop) {
+ BackStackRecord inTransaction = fragments.lastInTransaction;
+ if (enterTransition != null && inSharedElements != null
+ && inTransaction.mSharedElementSourceNames != null
+ && !inTransaction.mSharedElementSourceNames.isEmpty()) {
+ final String targetName = inIsPop
+ ? inTransaction.mSharedElementSourceNames.get(0)
+ : inTransaction.mSharedElementTargetNames.get(0);
+ return inSharedElements.get(targetName);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the epicenter for the exit transition.
+ *
+ * @param sharedElementTransition The shared element transition
+ * @param exitTransition The transition for the outgoing fragment's views
+ * @param outSharedElements Shared elements in the outgoing fragment
+ * @param outIsPop Is the outgoing fragment being removed as a pop transaction?
+ * @param outTransaction The transaction that caused the fragment to be removed.
+ */
+ private static void setOutEpicenter(TransitionSet sharedElementTransition,
+ Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,
+ BackStackRecord outTransaction) {
+ if (outTransaction.mSharedElementSourceNames != null &&
+ !outTransaction.mSharedElementSourceNames.isEmpty()) {
+ final String sourceName = outIsPop
+ ? outTransaction.mSharedElementTargetNames.get(0)
+ : outTransaction.mSharedElementSourceNames.get(0);
+ final View outEpicenterView = outSharedElements.get(sourceName);
+ setEpicenter(sharedElementTransition, outEpicenterView);
+
+ if (exitTransition != null) {
+ setEpicenter(exitTransition, outEpicenterView);
+ }
+ }
+ }
+
+ /**
+ * Sets a transition epicenter to the rectangle of a given View.
+ */
+ private static void setEpicenter(Transition transition, View view) {
+ if (view != null) {
+ final Rect epicenter = new Rect();
+ view.getBoundsOnScreen(epicenter);
+
+ transition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+ }
+ }
+
+ /**
+ * A utility to retain only the mappings in {@code nameOverrides} that have a value
+ * that has a key in {@code namedViews}. This is a useful equivalent to
+ * {@link ArrayMap#retainAll(Collection)} for values.
+ */
+ private static void retainValues(ArrayMap<String, String> nameOverrides,
+ ArrayMap<String, View> namedViews) {
+ for (int i = nameOverrides.size() - 1; i >= 0; i--) {
+ final String targetName = nameOverrides.valueAt(i);
+ if (!namedViews.containsKey(targetName)) {
+ nameOverrides.removeAt(i);
+ }
+ }
+ }
+
+ /**
+ * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or
+ * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate
+ * incoming or outgoing fragment.
+ *
+ * @param inFragment The incoming fragment
+ * @param outFragment The outgoing fragment
+ * @param isPop Is the incoming fragment part of a pop transaction?
+ * @param sharedElements The shared element Views
+ * @param isStart Call the start or end call on the SharedElementCallback
+ */
+ private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,
+ boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {
+ SharedElementCallback sharedElementCallback = isPop
+ ? outFragment.getEnterTransitionCallback()
+ : inFragment.getEnterTransitionCallback();
+ if (sharedElementCallback != null) {
+ ArrayList<View> views = new ArrayList<>();
+ ArrayList<String> names = new ArrayList<>();
+ final int count = sharedElements == null ? 0 : sharedElements.size();
+ for (int i = 0; i < count; i++) {
+ names.add(sharedElements.keyAt(i));
+ views.add(sharedElements.valueAt(i));
+ }
+ if (isStart) {
+ sharedElementCallback.onSharedElementStart(names, views, null);
+ } else {
+ sharedElementCallback.onSharedElementEnd(names, views, null);
+ }
+ }
+ }
+
+ /**
+ * Finds all children of the shared elements and sets the wrapping TransitionSet
+ * targets to point to those. It also limits transitions that have no targets to the
+ * specific shared elements. This allows developers to target child views of the
+ * shared elements specifically, but this doesn't happen by default.
+ */
+ private static void setSharedElementTargets(TransitionSet transition,
+ View nonExistentView, ArrayList<View> sharedViews) {
+ final List<View> views = transition.getTargets();
+ views.clear();
+ final int count = sharedViews.size();
+ for (int i = 0; i < count; i++) {
+ final View view = sharedViews.get(i);
+ bfsAddViewChildren(views, view);
+ }
+ views.add(nonExistentView);
+ sharedViews.add(nonExistentView);
+ addTargets(transition, sharedViews);
+ }
+
+ /**
+ * Uses a breadth-first scheme to add startView and all of its children to views.
+ * It won't add a child if it is already in views.
+ */
+ private static void bfsAddViewChildren(final List<View> views, final View startView) {
+ final int startIndex = views.size();
+ if (containedBeforeIndex(views, startView, startIndex)) {
+ return; // This child is already in the list, so all its children are also.
+ }
+ views.add(startView);
+ for (int index = startIndex; index < views.size(); index++) {
+ final View view = views.get(index);
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ final int childCount = viewGroup.getChildCount();
+ for (int childIndex = 0; childIndex < childCount; childIndex++) {
+ final View child = viewGroup.getChildAt(childIndex);
+ if (!containedBeforeIndex(views, child, startIndex)) {
+ views.add(child);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Does a linear search through views for view, limited to maxIndex.
+ */
+ private static boolean containedBeforeIndex(final List<View> views, final View view,
+ final int maxIndex) {
+ for (int i = 0; i < maxIndex; i++) {
+ if (views.get(i) == view) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * After the transition has started, remove all targets that we added to the transitions
+ * so that the transitions are left in a clean state.
+ */
+ private static void scheduleRemoveTargets(final Transition overalTransition,
+ final Transition enterTransition, final ArrayList<View> enteringViews,
+ final Transition exitTransition, final ArrayList<View> exitingViews,
+ final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn) {
+ overalTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ if (enterTransition != null) {
+ replaceTargets(enterTransition, enteringViews, null);
+ }
+ if (exitTransition != null) {
+ replaceTargets(exitTransition, exitingViews, null);
+ }
+ if (sharedElementTransition != null) {
+ replaceTargets(sharedElementTransition, sharedElementsIn, null);
+ }
+ }
+ });
+ }
+
+ /**
+ * This method removes the views from transitions that target ONLY those views and
+ * replaces them with the new targets list.
+ * The views list should match those added in addTargets and should contain
+ * one view that is not in the view hierarchy (state.nonExistentView).
+ */
+ public static void replaceTargets(Transition transition, ArrayList<View> oldTargets,
+ ArrayList<View> newTargets) {
+ if (transition instanceof TransitionSet) {
+ TransitionSet set = (TransitionSet) transition;
+ int numTransitions = set.getTransitionCount();
+ for (int i = 0; i < numTransitions; i++) {
+ Transition child = set.getTransitionAt(i);
+ replaceTargets(child, oldTargets, newTargets);
+ }
+ } else if (!hasSimpleTarget(transition)) {
+ List<View> targets = transition.getTargets();
+ if (targets != null && targets.size() == oldTargets.size() &&
+ targets.containsAll(oldTargets)) {
+ // We have an exact match. We must have added these earlier in addTargets
+ final int targetCount = newTargets == null ? 0 : newTargets.size();
+ for (int i = 0; i < targetCount; i++) {
+ transition.addTarget(newTargets.get(i));
+ }
+ for (int i = oldTargets.size() - 1; i >= 0; i--) {
+ transition.removeTarget(oldTargets.get(i));
+ }
+ }
+ }
+ }
+
+ /**
+ * This method adds views as targets to the transition, but only if the transition
+ * doesn't already have a target. It is best for views to contain one View object
+ * that does not exist in the view hierarchy (state.nonExistentView) so that
+ * when they are removed later, a list match will suffice to remove the targets.
+ * Otherwise, if you happened to have targeted the exact views for the transition,
+ * the replaceTargets call will remove them unexpectedly.
+ */
+ public static void addTargets(Transition transition, ArrayList<View> views) {
+ if (transition == null) {
+ return;
+ }
+ if (transition instanceof TransitionSet) {
+ TransitionSet set = (TransitionSet) transition;
+ int numTransitions = set.getTransitionCount();
+ for (int i = 0; i < numTransitions; i++) {
+ Transition child = set.getTransitionAt(i);
+ addTargets(child, views);
+ }
+ } else if (!hasSimpleTarget(transition)) {
+ List<View> targets = transition.getTargets();
+ if (isNullOrEmpty(targets)) {
+ // We can just add the target views
+ int numViews = views.size();
+ for (int i = 0; i < numViews; i++) {
+ transition.addTarget(views.get(i));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if there are any targets based on ID, transition or type.
+ */
+ private static boolean hasSimpleTarget(Transition transition) {
+ return !isNullOrEmpty(transition.getTargetIds()) ||
+ !isNullOrEmpty(transition.getTargetNames()) ||
+ !isNullOrEmpty(transition.getTargetTypes());
+ }
+
+ /**
+ * Simple utility to detect if a list is null or has no elements.
+ */
+ private static boolean isNullOrEmpty(List list) {
+ return list == null || list.isEmpty();
+ }
+
+ private static ArrayList<View> configureEnteringExitingViews(Transition transition,
+ Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
+ ArrayList<View> viewList = null;
+ if (transition != null) {
+ viewList = new ArrayList<>();
+ View root = fragment.getView();
+ if (root != null) {
+ root.captureTransitioningViews(viewList);
+ }
+ if (sharedElements != null) {
+ viewList.removeAll(sharedElements);
+ }
+ if (!viewList.isEmpty()) {
+ viewList.add(nonExistentView);
+ addTargets(transition, viewList);
+ }
+ }
+ return viewList;
+ }
+
+ /**
+ * Sets the visibility of all Views in {@code views} to {@code visibility}.
+ */
+ private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) {
+ if (views == null) {
+ return;
+ }
+ for (int i = views.size() - 1; i >= 0; i--) {
+ final View view = views.get(i);
+ view.setVisibility(visibility);
+ }
+ }
+
+ /**
+ * Merges exit, shared element, and enter transitions so that they act together or
+ * sequentially as defined in the fragments.
+ */
+ private static Transition mergeTransitions(Transition enterTransition,
+ Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
+ boolean isPop) {
+ boolean overlap = true;
+ if (enterTransition != null && exitTransition != null && inFragment != null) {
+ overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
+ inFragment.getAllowEnterTransitionOverlap();
+ }
+
+ // Wrap the transitions. Explicit targets like in enter and exit will cause the
+ // views to be targeted regardless of excluded views. If that happens, then the
+ // excluded fragments views (hidden fragments) will still be in the transition.
+
+ Transition transition;
+ if (overlap) {
+ // Regular transition -- do it all together
+ TransitionSet transitionSet = new TransitionSet();
+ if (enterTransition != null) {
+ transitionSet.addTransition(enterTransition);
+ }
+ if (exitTransition != null) {
+ transitionSet.addTransition(exitTransition);
+ }
+ if (sharedElementTransition != null) {
+ transitionSet.addTransition(sharedElementTransition);
+ }
+ transition = transitionSet;
+ } else {
+ // First do exit, then enter, but allow shared element transition to happen
+ // during both.
+ Transition staggered = null;
+ if (exitTransition != null && enterTransition != null) {
+ staggered = new TransitionSet()
+ .addTransition(exitTransition)
+ .addTransition(enterTransition)
+ .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+ } else if (exitTransition != null) {
+ staggered = exitTransition;
+ } else if (enterTransition != null) {
+ staggered = enterTransition;
+ }
+ if (sharedElementTransition != null) {
+ TransitionSet together = new TransitionSet();
+ if (staggered != null) {
+ together.addTransition(staggered);
+ }
+ together.addTransition(sharedElementTransition);
+ transition = together;
+ } else {
+ transition = staggered;
+ }
+ }
+ return transition;
+ }
+
+ /**
+ * Finds the first removed fragment and last added fragments when going forward.
+ * If none of the fragments have transitions, then both lists will be empty.
+ *
+ * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
+ * and last fragments to be added. This will be modified by
+ * this method.
+ */
+ public static void calculateFragments(BackStackRecord transaction,
+ SparseArray<FragmentContainerTransition> transitioningFragments,
+ boolean isReordered) {
+ final int numOps = transaction.mOps.size();
+ for (int opNum = 0; opNum < numOps; opNum++) {
+ final BackStackRecord.Op op = transaction.mOps.get(opNum);
+ addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
+ }
+ }
+
+ /**
+ * Finds the first removed fragment and last added fragments when popping the back stack.
+ * If none of the fragments have transitions, then both lists will be empty.
+ *
+ * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
+ * and last fragments to be added. This will be modified by
+ * this method.
+ */
+ public static void calculatePopFragments(BackStackRecord transaction,
+ SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
+ if (!transaction.mManager.mContainer.onHasView()) {
+ return; // nothing to see, so no transitions
+ }
+ final int numOps = transaction.mOps.size();
+ for (int opNum = numOps - 1; opNum >= 0; opNum--) {
+ final BackStackRecord.Op op = transaction.mOps.get(opNum);
+ addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
+ }
+ }
+
+ /**
+ * Examines the {@code command} and may set the first out or last in fragment for the fragment's
+ * container.
+ *
+ * @param transaction The executing transaction
+ * @param op The operation being run.
+ * @param transitioningFragments A structure holding the first in and last out fragments
+ * for each fragment container.
+ * @param isPop Is the operation a pop?
+ * @param isReorderedTransaction True if the operations have been partially executed and the
+ * added fragments have Views in the hierarchy or false if the
+ * operations haven't been executed yet.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
+ SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
+ boolean isReorderedTransaction) {
+ final Fragment fragment = op.fragment;
+ if (fragment == null) {
+ return; // no fragment, no transition
+ }
+ final int containerId = fragment.mContainerId;
+ if (containerId == 0) {
+ return; // no container, no transition
+ }
+ final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
+ boolean setLastIn = false;
+ boolean wasRemoved = false;
+ boolean setFirstOut = false;
+ boolean wasAdded = false;
+ switch (command) {
+ case BackStackRecord.OP_SHOW:
+ if (isReorderedTransaction) {
+ setLastIn = fragment.mHiddenChanged && !fragment.mHidden &&
+ fragment.mAdded;
+ } else {
+ setLastIn = fragment.mHidden;
+ }
+ wasAdded = true;
+ break;
+ case BackStackRecord.OP_ADD:
+ case BackStackRecord.OP_ATTACH:
+ if (isReorderedTransaction) {
+ setLastIn = fragment.mIsNewlyAdded;
+ } else {
+ setLastIn = !fragment.mAdded && !fragment.mHidden;
+ }
+ wasAdded = true;
+ break;
+ case BackStackRecord.OP_HIDE:
+ if (isReorderedTransaction) {
+ setFirstOut = fragment.mHiddenChanged && fragment.mAdded &&
+ fragment.mHidden;
+ } else {
+ setFirstOut = fragment.mAdded && !fragment.mHidden;
+ }
+ wasRemoved = true;
+ break;
+ case BackStackRecord.OP_REMOVE:
+ case BackStackRecord.OP_DETACH:
+ if (isReorderedTransaction) {
+ setFirstOut = !fragment.mAdded && fragment.mView != null
+ && fragment.mView.getVisibility() == View.VISIBLE
+ && fragment.mView.getTransitionAlpha() > 0;
+ } else {
+ setFirstOut = fragment.mAdded && !fragment.mHidden;
+ }
+ wasRemoved = true;
+ break;
+ }
+ FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
+ if (setLastIn) {
+ containerTransition =
+ ensureContainer(containerTransition, transitioningFragments, containerId);
+ containerTransition.lastIn = fragment;
+ containerTransition.lastInIsPop = isPop;
+ containerTransition.lastInTransaction = transaction;
+ }
+ if (!isReorderedTransaction && wasAdded) {
+ if (containerTransition != null && containerTransition.firstOut == fragment) {
+ containerTransition.firstOut = null;
+ }
+
+ /*
+ * Ensure that fragments that are entering are at least at the CREATED state
+ * so that they may load Transitions using TransitionInflater.
+ */
+ FragmentManagerImpl manager = transaction.mManager;
+ if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED &&
+ manager.mHost.getContext().getApplicationInfo().targetSdkVersion >=
+ Build.VERSION_CODES.N && !transaction.mReorderingAllowed) {
+ manager.makeActive(fragment);
+ manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
+ }
+ }
+ if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
+ containerTransition =
+ ensureContainer(containerTransition, transitioningFragments, containerId);
+ containerTransition.firstOut = fragment;
+ containerTransition.firstOutIsPop = isPop;
+ containerTransition.firstOutTransaction = transaction;
+ }
+
+ if (!isReorderedTransaction && wasRemoved &&
+ (containerTransition != null && containerTransition.lastIn == fragment)) {
+ containerTransition.lastIn = null;
+ }
+ }
+
+ /**
+ * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
+ * it returns the existing one. If not, one is created and added to the SparseArray and
+ * returned.
+ */
+ private static FragmentContainerTransition ensureContainer(
+ FragmentContainerTransition containerTransition,
+ SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
+ if (containerTransition == null) {
+ containerTransition = new FragmentContainerTransition();
+ transitioningFragments.put(containerId, containerTransition);
+ }
+ return containerTransition;
+ }
+
+ /**
+ * Tracks the last fragment added and first fragment removed for fragment transitions.
+ * This also tracks which fragments are changed by push or pop transactions.
+ */
+ public static class FragmentContainerTransition {
+ /**
+ * The last fragment added/attached/shown in its container
+ */
+ public Fragment lastIn;
+
+ /**
+ * true when lastIn was added during a pop transaction or false if added with a push
+ */
+ public boolean lastInIsPop;
+
+ /**
+ * The transaction that included the last in fragment
+ */
+ public BackStackRecord lastInTransaction;
+
+ /**
+ * The first fragment with a View that was removed/detached/hidden in its container.
+ */
+ public Fragment firstOut;
+
+ /**
+ * true when firstOut was removed during a pop transaction or false otherwise
+ */
+ public boolean firstOutIsPop;
+
+ /**
+ * The transaction that included the first out fragment
+ */
+ public BackStackRecord firstOutTransaction;
+ }
+}
diff --git a/android/app/Fragment_Delegate.java b/android/app/Fragment_Delegate.java
new file mode 100644
index 00000000..f7654ce2
--- /dev/null
+++ b/android/app/Fragment_Delegate.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.ide.common.rendering.api.LayoutlibCallback;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.os.Bundle;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link Fragment}
+ *
+ * Through the layoutlib_create tool, the original methods of Fragment have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * The methods being re-implemented are the ones responsible for instantiating Fragment objects.
+ * Because the classes of these objects are found in the project, these methods need access to
+ * {@link LayoutlibCallback} object. They are however static methods, so the callback is set
+ * before the inflation through {@link #setLayoutlibCallback(LayoutlibCallback)}.
+ */
+public class Fragment_Delegate {
+
+ private static LayoutlibCallback sLayoutlibCallback;
+
+ /**
+ * Sets the current {@link LayoutlibCallback} to be used to instantiate classes coming
+ * from the project being rendered.
+ */
+ public static void setLayoutlibCallback(LayoutlibCallback layoutlibCallback) {
+ sLayoutlibCallback = layoutlibCallback;
+ }
+
+ /**
+ * Like {@link #instantiate(Context, String, Bundle)} but with a null
+ * argument Bundle.
+ */
+ @LayoutlibDelegate
+ /*package*/ static Fragment instantiate(Context context, String fname) {
+ return instantiate(context, fname, null);
+ }
+
+ /**
+ * Create a new instance of a Fragment with the given class name. This is
+ * the same as calling its empty constructor.
+ *
+ * @param context The calling context being used to instantiate the fragment.
+ * This is currently just used to get its ClassLoader.
+ * @param fname The class name of the fragment to instantiate.
+ * @param args Bundle of arguments to supply to the fragment, which it
+ * can retrieve with {@link Fragment#getArguments()}. May be null.
+ * @return Returns a new fragment instance.
+ * @throws Fragment.InstantiationException If there is a failure in instantiating
+ * the given fragment class. This is a runtime exception; it is not
+ * normally expected to happen.
+ */
+ @LayoutlibDelegate
+ /*package*/ static Fragment instantiate(Context context, String fname, Bundle args) {
+ try {
+ if (sLayoutlibCallback != null) {
+ Fragment f = (Fragment) sLayoutlibCallback.loadView(fname,
+ new Class[0], new Object[0]);
+
+ if (args != null) {
+ args.setClassLoader(f.getClass().getClassLoader());
+ f.mArguments = args;
+ }
+ return f;
+ }
+
+ return null;
+ } catch (ClassNotFoundException e) {
+ throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (java.lang.InstantiationException e) {
+ throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (IllegalAccessException e) {
+ throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ } catch (Exception e) {
+ throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname
+ + ": make sure class name exists, is public, and has an"
+ + " empty constructor that is public", e);
+ }
+ }
+}
diff --git a/android/app/InstantAppResolverService.java b/android/app/InstantAppResolverService.java
new file mode 100644
index 00000000..c5dc86c7
--- /dev/null
+++ b/android/app/InstantAppResolverService.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.InstantAppResolveInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.os.SomeArgs;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Base class for implementing the resolver service.
+ * @hide
+ */
+@SystemApi
+public abstract class InstantAppResolverService extends Service {
+ private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE;
+ private static final String TAG = "PackageManager";
+
+ /** @hide */
+ public static final String EXTRA_RESOLVE_INFO = "android.app.extra.RESOLVE_INFO";
+ /** @hide */
+ public static final String EXTRA_SEQUENCE = "android.app.extra.SEQUENCE";
+ Handler mHandler;
+
+ /**
+ * Called to retrieve resolve info for instant applications.
+ *
+ * @param digestPrefix The hash prefix of the instant app's domain.
+ */
+ public void onGetInstantAppResolveInfo(
+ int digestPrefix[], String token, InstantAppResolutionCallback callback) {
+ throw new IllegalStateException("Must define");
+ }
+
+ /**
+ * Called to retrieve intent filters for instant applications.
+ *
+ * @param digestPrefix The hash prefix of the instant app's domain.
+ */
+ public void onGetInstantAppIntentFilter(
+ int digestPrefix[], String token, InstantAppResolutionCallback callback) {
+ throw new IllegalStateException("Must define");
+ }
+
+ /**
+ * Returns a {@link Looper} to perform service operations on.
+ */
+ Looper getLooper() {
+ return getBaseContext().getMainLooper();
+ }
+
+ @Override
+ public final void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ mHandler = new ServiceHandler(getLooper());
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return new IInstantAppResolver.Stub() {
+ @Override
+ public void getInstantAppResolveInfoList(
+ int digestPrefix[], String token, int sequence, IRemoteCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.v(TAG, "[" + token + "] Phase1 called; posting");
+ }
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callback;
+ args.arg2 = digestPrefix;
+ args.arg3 = token;
+ mHandler.obtainMessage(
+ ServiceHandler.MSG_GET_INSTANT_APP_RESOLVE_INFO, sequence, 0, args)
+ .sendToTarget();
+ }
+
+ @Override
+ public void getInstantAppIntentFilterList(
+ int digestPrefix[], String token, String hostName, IRemoteCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.v(TAG, "[" + token + "] Phase2 called; posting");
+ }
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callback;
+ args.arg2 = digestPrefix;
+ args.arg3 = token;
+ args.arg4 = hostName;
+ mHandler.obtainMessage(
+ ServiceHandler.MSG_GET_INSTANT_APP_INTENT_FILTER, callback).sendToTarget();
+ }
+ };
+ }
+
+ /**
+ * Callback to post results from instant app resolution.
+ */
+ public static final class InstantAppResolutionCallback {
+ private final IRemoteCallback mCallback;
+ private final int mSequence;
+ InstantAppResolutionCallback(int sequence, IRemoteCallback callback) {
+ mCallback = callback;
+ mSequence = sequence;
+ }
+
+ public void onInstantAppResolveInfo(List<InstantAppResolveInfo> resolveInfo) {
+ final Bundle data = new Bundle();
+ data.putParcelableList(EXTRA_RESOLVE_INFO, resolveInfo);
+ data.putInt(EXTRA_SEQUENCE, mSequence);
+ try {
+ mCallback.sendResult(data);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Deprecated
+ void _onGetInstantAppResolveInfo(int[] digestPrefix, String token,
+ InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.d(TAG, "[" + token + "] Phase1 request;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ onGetInstantAppResolveInfo(digestPrefix, token, callback);
+ }
+ @Deprecated
+ void _onGetInstantAppIntentFilter(int digestPrefix[], String token, String hostName,
+ InstantAppResolutionCallback callback) {
+ if (DEBUG_EPHEMERAL) {
+ Slog.d(TAG, "[" + token + "] Phase2 request;"
+ + " prefix: " + Arrays.toString(digestPrefix));
+ }
+ onGetInstantAppIntentFilter(digestPrefix, token, callback);
+ }
+
+ private final class ServiceHandler extends Handler {
+ public static final int MSG_GET_INSTANT_APP_RESOLVE_INFO = 1;
+ public static final int MSG_GET_INSTANT_APP_INTENT_FILTER = 2;
+
+ public ServiceHandler(Looper looper) {
+ super(looper, null /*callback*/, true /*async*/);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(Message message) {
+ final int action = message.what;
+ switch (action) {
+ case MSG_GET_INSTANT_APP_RESOLVE_INFO: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final IRemoteCallback callback = (IRemoteCallback) args.arg1;
+ final int[] digestPrefix = (int[]) args.arg2;
+ final String token = (String) args.arg3;
+ final int sequence = message.arg1;
+ _onGetInstantAppResolveInfo(
+ digestPrefix, token,
+ new InstantAppResolutionCallback(sequence, callback));
+ } break;
+
+ case MSG_GET_INSTANT_APP_INTENT_FILTER: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final IRemoteCallback callback = (IRemoteCallback) args.arg1;
+ final int[] digestPrefix = (int[]) args.arg2;
+ final String token = (String) args.arg3;
+ final String hostName = (String) args.arg4;
+ _onGetInstantAppIntentFilter(
+ digestPrefix, token, hostName,
+ new InstantAppResolutionCallback(-1 /*sequence*/, callback));
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Unknown message: " + action);
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java
new file mode 100644
index 00000000..e260967f
--- /dev/null
+++ b/android/app/Instrumentation.java
@@ -0,0 +1,2168 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.os.PerformanceCollector;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.TestLooperManager;
+import android.os.UserHandle;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.view.IWindowManager;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.Window;
+
+import com.android.internal.content.ReferrerIntent;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class for implementing application instrumentation code. When running
+ * with instrumentation turned on, this class will be instantiated for you
+ * before any of the application code, allowing you to monitor all of the
+ * interaction the system has with the application. An Instrumentation
+ * implementation is described to the system through an AndroidManifest.xml's
+ * &lt;instrumentation&gt; tag.
+ */
+public class Instrumentation {
+
+ /**
+ * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+ * identifies the class that is writing the report. This can be used to provide more structured
+ * logging or reporting capabilities in the IInstrumentationWatcher.
+ */
+ public static final String REPORT_KEY_IDENTIFIER = "id";
+ /**
+ * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+ * identifies a string which can simply be printed to the output stream. Using these streams
+ * provides a "pretty printer" version of the status & final packets. Any bundles including
+ * this key should also include the complete set of raw key/value pairs, so that the
+ * instrumentation can also be launched, and results collected, by an automated system.
+ */
+ public static final String REPORT_KEY_STREAMRESULT = "stream";
+
+ private static final String TAG = "Instrumentation";
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({0, UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES})
+ public @interface UiAutomationFlags {};
+
+
+ private final Object mSync = new Object();
+ private ActivityThread mThread = null;
+ private MessageQueue mMessageQueue = null;
+ private Context mInstrContext;
+ private Context mAppContext;
+ private ComponentName mComponent;
+ private Thread mRunner;
+ private List<ActivityWaiter> mWaitingActivities;
+ private List<ActivityMonitor> mActivityMonitors;
+ private IInstrumentationWatcher mWatcher;
+ private IUiAutomationConnection mUiAutomationConnection;
+ private boolean mAutomaticPerformanceSnapshots = false;
+ private PerformanceCollector mPerformanceCollector;
+ private Bundle mPerfMetrics = new Bundle();
+ private UiAutomation mUiAutomation;
+
+ public Instrumentation() {
+ }
+
+ /**
+ * Called for methods that shouldn't be called by standard apps and
+ * should only be used in instrumentation environments. This is not
+ * security feature as these classes will still be accessible through
+ * reflection, but it will serve as noticeable discouragement from
+ * doing such a thing.
+ */
+ private void checkInstrumenting(String method) {
+ // Check if we have an instrumentation context, as init should only get called by
+ // the system in startup processes that are being instrumented.
+ if (mInstrContext == null) {
+ throw new RuntimeException(method +
+ " cannot be called outside of instrumented processes");
+ }
+ }
+
+ /**
+ * Called when the instrumentation is starting, before any application code
+ * has been loaded. Usually this will be implemented to simply call
+ * {@link #start} to begin the instrumentation thread, which will then
+ * continue execution in {@link #onStart}.
+ *
+ * <p>If you do not need your own thread -- that is you are writing your
+ * instrumentation to be completely asynchronous (returning to the event
+ * loop so that the application can run), you can simply begin your
+ * instrumentation here, for example call {@link Context#startActivity} to
+ * begin the appropriate first activity of the application.
+ *
+ * @param arguments Any additional arguments that were supplied when the
+ * instrumentation was started.
+ */
+ public void onCreate(Bundle arguments) {
+ }
+
+ /**
+ * Create and start a new thread in which to run instrumentation. This new
+ * thread will call to {@link #onStart} where you can implement the
+ * instrumentation.
+ */
+ public void start() {
+ if (mRunner != null) {
+ throw new RuntimeException("Instrumentation already started");
+ }
+ mRunner = new InstrumentationThread("Instr: " + getClass().getName());
+ mRunner.start();
+ }
+
+ /**
+ * Method where the instrumentation thread enters execution. This allows
+ * you to run your instrumentation code in a separate thread than the
+ * application, so that it can perform blocking operation such as
+ * {@link #sendKeySync} or {@link #startActivitySync}.
+ *
+ * <p>You will typically want to call finish() when this function is done,
+ * to end your instrumentation.
+ */
+ public void onStart() {
+ }
+
+ /**
+ * This is called whenever the system captures an unhandled exception that
+ * was thrown by the application. The default implementation simply
+ * returns false, allowing normal system handling of the exception to take
+ * place.
+ *
+ * @param obj The client object that generated the exception. May be an
+ * Application, Activity, BroadcastReceiver, Service, or null.
+ * @param e The exception that was thrown.
+ *
+ * @return To allow normal system exception process to occur, return false.
+ * If true is returned, the system will proceed as if the exception
+ * didn't happen.
+ */
+ public boolean onException(Object obj, Throwable e) {
+ return false;
+ }
+
+ /**
+ * Provide a status report about the application.
+ *
+ * @param resultCode Current success/failure of instrumentation.
+ * @param results Any results to send back to the code that started the instrumentation.
+ */
+ public void sendStatus(int resultCode, Bundle results) {
+ if (mWatcher != null) {
+ try {
+ mWatcher.instrumentationStatus(mComponent, resultCode, results);
+ }
+ catch (RemoteException e) {
+ mWatcher = null;
+ }
+ }
+ }
+
+ /**
+ * Report some results in the middle of instrumentation execution. Later results (including
+ * those provided by {@link #finish}) will be combined with {@link Bundle#putAll}.
+ */
+ public void addResults(Bundle results) {
+ IActivityManager am = ActivityManager.getService();
+ try {
+ am.addInstrumentationResults(mThread.getApplicationThread(), results);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Terminate instrumentation of the application. This will cause the
+ * application process to exit, removing this instrumentation from the next
+ * time the application is started. If multiple processes are currently running
+ * for this instrumentation, all of those processes will be killed.
+ *
+ * @param resultCode Overall success/failure of instrumentation.
+ * @param results Any results to send back to the code that started the
+ * instrumentation.
+ */
+ public void finish(int resultCode, Bundle results) {
+ if (mAutomaticPerformanceSnapshots) {
+ endPerformanceSnapshot();
+ }
+ if (mPerfMetrics != null) {
+ if (results == null) {
+ results = new Bundle();
+ }
+ results.putAll(mPerfMetrics);
+ }
+ if ((mUiAutomation != null) && !mUiAutomation.isDestroyed()) {
+ mUiAutomation.disconnect();
+ mUiAutomation = null;
+ }
+ mThread.finishInstrumentation(resultCode, results);
+ }
+
+ public void setAutomaticPerformanceSnapshots() {
+ mAutomaticPerformanceSnapshots = true;
+ mPerformanceCollector = new PerformanceCollector();
+ }
+
+ public void startPerformanceSnapshot() {
+ if (!isProfiling()) {
+ mPerformanceCollector.beginSnapshot(null);
+ }
+ }
+
+ public void endPerformanceSnapshot() {
+ if (!isProfiling()) {
+ mPerfMetrics = mPerformanceCollector.endSnapshot();
+ }
+ }
+
+ /**
+ * Called when the instrumented application is stopping, after all of the
+ * normal application cleanup has occurred.
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * Return the Context of this instrumentation's package. Note that this is
+ * often different than the Context of the application being
+ * instrumentated, since the instrumentation code often lives is a
+ * different package than that of the application it is running against.
+ * See {@link #getTargetContext} to retrieve a Context for the target
+ * application.
+ *
+ * @return The instrumentation's package context.
+ *
+ * @see #getTargetContext
+ */
+ public Context getContext() {
+ return mInstrContext;
+ }
+
+ /**
+ * Returns complete component name of this instrumentation.
+ *
+ * @return Returns the complete component name for this instrumentation.
+ */
+ public ComponentName getComponentName() {
+ return mComponent;
+ }
+
+ /**
+ * Return a Context for the target application being instrumented. Note
+ * that this is often different than the Context of the instrumentation
+ * code, since the instrumentation code often lives is a different package
+ * than that of the application it is running against. See
+ * {@link #getContext} to retrieve a Context for the instrumentation code.
+ *
+ * @return A Context in the target application.
+ *
+ * @see #getContext
+ */
+ public Context getTargetContext() {
+ return mAppContext;
+ }
+
+ /**
+ * Return the name of the process this instrumentation is running in. Note this should
+ * only be used for testing and debugging. If you are thinking about using this to,
+ * for example, conditionalize what is initialized in an Application class, it is strongly
+ * recommended to instead use lazy initialization (such as a getter for the state that
+ * only creates it when requested). This can greatly reduce the work your process does
+ * when created for secondary things, such as to receive a broadcast.
+ */
+ public String getProcessName() {
+ return mThread.getProcessName();
+ }
+
+ /**
+ * Check whether this instrumentation was started with profiling enabled.
+ *
+ * @return Returns true if profiling was enabled when starting, else false.
+ */
+ public boolean isProfiling() {
+ return mThread.isProfiling();
+ }
+
+ /**
+ * This method will start profiling if isProfiling() returns true. You should
+ * only call this method if you set the handleProfiling attribute in the
+ * manifest file for this Instrumentation to true.
+ */
+ public void startProfiling() {
+ if (mThread.isProfiling()) {
+ File file = new File(mThread.getProfileFilePath());
+ file.getParentFile().mkdirs();
+ Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
+ }
+ }
+
+ /**
+ * Stops profiling if isProfiling() returns true.
+ */
+ public void stopProfiling() {
+ if (mThread.isProfiling()) {
+ Debug.stopMethodTracing();
+ }
+ }
+
+ /**
+ * Force the global system in or out of touch mode. This can be used if
+ * your instrumentation relies on the UI being in one more or the other
+ * when it starts.
+ *
+ * @param inTouch Set to true to be in touch mode, false to be in
+ * focus mode.
+ */
+ public void setInTouchMode(boolean inTouch) {
+ try {
+ IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window")).setInTouchMode(inTouch);
+ } catch (RemoteException e) {
+ // Shouldn't happen!
+ }
+ }
+
+ /**
+ * Schedule a callback for when the application's main thread goes idle
+ * (has no more events to process).
+ *
+ * @param recipient Called the next time the thread's message queue is
+ * idle.
+ */
+ public void waitForIdle(Runnable recipient) {
+ mMessageQueue.addIdleHandler(new Idler(recipient));
+ mThread.getHandler().post(new EmptyRunnable());
+ }
+
+ /**
+ * Synchronously wait for the application to be idle. Can not be called
+ * from the main application thread -- use {@link #start} to execute
+ * instrumentation in its own thread.
+ */
+ public void waitForIdleSync() {
+ validateNotAppThread();
+ Idler idler = new Idler(null);
+ mMessageQueue.addIdleHandler(idler);
+ mThread.getHandler().post(new EmptyRunnable());
+ idler.waitForIdle();
+ }
+
+ /**
+ * Execute a call on the application's main thread, blocking until it is
+ * complete. Useful for doing things that are not thread-safe, such as
+ * looking at or modifying the view hierarchy.
+ *
+ * @param runner The code to run on the main thread.
+ */
+ public void runOnMainSync(Runnable runner) {
+ validateNotAppThread();
+ SyncRunnable sr = new SyncRunnable(runner);
+ mThread.getHandler().post(sr);
+ sr.waitForComplete();
+ }
+
+ /**
+ * Start a new activity and wait for it to begin running before returning.
+ * In addition to being synchronous, this method as some semantic
+ * differences from the standard {@link Context#startActivity} call: the
+ * activity component is resolved before talking with the activity manager
+ * (its class name is specified in the Intent that this method ultimately
+ * starts), and it does not allow you to start activities that run in a
+ * different process. In addition, if the given Intent resolves to
+ * multiple activities, instead of displaying a dialog for the user to
+ * select an activity, an exception will be thrown.
+ *
+ * <p>The function returns as soon as the activity goes idle following the
+ * call to its {@link Activity#onCreate}. Generally this means it has gone
+ * through the full initialization including {@link Activity#onResume} and
+ * drawn and displayed its initial window.
+ *
+ * @param intent Description of the activity to start.
+ *
+ * @see Context#startActivity
+ */
+ public Activity startActivitySync(Intent intent) {
+ validateNotAppThread();
+
+ synchronized (mSync) {
+ intent = new Intent(intent);
+
+ ActivityInfo ai = intent.resolveActivityInfo(
+ getTargetContext().getPackageManager(), 0);
+ if (ai == null) {
+ throw new RuntimeException("Unable to resolve activity for: " + intent);
+ }
+ String myProc = mThread.getProcessName();
+ if (!ai.processName.equals(myProc)) {
+ // todo: if this intent is ambiguous, look here to see if
+ // there is a single match that is in our package.
+ throw new RuntimeException("Intent in process "
+ + myProc + " resolved to different process "
+ + ai.processName + ": " + intent);
+ }
+
+ intent.setComponent(new ComponentName(
+ ai.applicationInfo.packageName, ai.name));
+ final ActivityWaiter aw = new ActivityWaiter(intent);
+
+ if (mWaitingActivities == null) {
+ mWaitingActivities = new ArrayList();
+ }
+ mWaitingActivities.add(aw);
+
+ getTargetContext().startActivity(intent);
+
+ do {
+ try {
+ mSync.wait();
+ } catch (InterruptedException e) {
+ }
+ } while (mWaitingActivities.contains(aw));
+
+ return aw.activity;
+ }
+ }
+
+ /**
+ * Information about a particular kind of Intent that is being monitored.
+ * An instance of this class is added to the
+ * current instrumentation through {@link #addMonitor}; after being added,
+ * when a new activity is being started the monitor will be checked and, if
+ * matching, its hit count updated and (optionally) the call stopped and a
+ * canned result returned.
+ *
+ * <p>An ActivityMonitor can also be used to look for the creation of an
+ * activity, through the {@link #waitForActivity} method. This will return
+ * after a matching activity has been created with that activity object.
+ */
+ public static class ActivityMonitor {
+ private final IntentFilter mWhich;
+ private final String mClass;
+ private final ActivityResult mResult;
+ private final boolean mBlock;
+ private final boolean mIgnoreMatchingSpecificIntents;
+
+
+ // This is protected by 'Instrumentation.this.mSync'.
+ /*package*/ int mHits = 0;
+
+ // This is protected by 'this'.
+ /*package*/ Activity mLastActivity = null;
+
+ /**
+ * Create a new ActivityMonitor that looks for a particular kind of
+ * intent to be started.
+ *
+ * @param which The set of intents this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @see Instrumentation#addMonitor
+ */
+ public ActivityMonitor(
+ IntentFilter which, ActivityResult result, boolean block) {
+ mWhich = which;
+ mClass = null;
+ mResult = result;
+ mBlock = block;
+ mIgnoreMatchingSpecificIntents = false;
+ }
+
+ /**
+ * Create a new ActivityMonitor that looks for a specific activity
+ * class to be started.
+ *
+ * @param cls The activity class this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @see Instrumentation#addMonitor
+ */
+ public ActivityMonitor(
+ String cls, ActivityResult result, boolean block) {
+ mWhich = null;
+ mClass = cls;
+ mResult = result;
+ mBlock = block;
+ mIgnoreMatchingSpecificIntents = false;
+ }
+
+ /**
+ * Create a new ActivityMonitor that can be used for intercepting any activity to be
+ * started.
+ *
+ * <p> When an activity is started, {@link #onStartActivity(Intent)} will be called on
+ * instances created using this constructor to see if it is a hit.
+ *
+ * @see #onStartActivity(Intent)
+ */
+ public ActivityMonitor() {
+ mWhich = null;
+ mClass = null;
+ mResult = null;
+ mBlock = false;
+ mIgnoreMatchingSpecificIntents = true;
+ }
+
+ /**
+ * @return true if this monitor is used for intercepting any started activity by calling
+ * into {@link #onStartActivity(Intent)}, false if this monitor is only used
+ * for specific intents corresponding to the intent filter or activity class
+ * passed in the constructor.
+ */
+ final boolean ignoreMatchingSpecificIntents() {
+ return mIgnoreMatchingSpecificIntents;
+ }
+
+ /**
+ * Retrieve the filter associated with this ActivityMonitor.
+ */
+ public final IntentFilter getFilter() {
+ return mWhich;
+ }
+
+ /**
+ * Retrieve the result associated with this ActivityMonitor, or null if
+ * none.
+ */
+ public final ActivityResult getResult() {
+ return mResult;
+ }
+
+ /**
+ * Check whether this monitor blocks activity starts (not allowing the
+ * actual activity to run) or allows them to execute normally.
+ */
+ public final boolean isBlocking() {
+ return mBlock;
+ }
+
+ /**
+ * Retrieve the number of times the monitor has been hit so far.
+ */
+ public final int getHits() {
+ return mHits;
+ }
+
+ /**
+ * Retrieve the most recent activity class that was seen by this
+ * monitor.
+ */
+ public final Activity getLastActivity() {
+ return mLastActivity;
+ }
+
+ /**
+ * Block until an Activity is created that matches this monitor,
+ * returning the resulting activity.
+ *
+ * @return Activity
+ */
+ public final Activity waitForActivity() {
+ synchronized (this) {
+ while (mLastActivity == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ Activity res = mLastActivity;
+ mLastActivity = null;
+ return res;
+ }
+ }
+
+ /**
+ * Block until an Activity is created that matches this monitor,
+ * returning the resulting activity or till the timeOut period expires.
+ * If the timeOut expires before the activity is started, return null.
+ *
+ * @param timeOut Time to wait in milliseconds before the activity is created.
+ *
+ * @return Activity
+ */
+ public final Activity waitForActivityWithTimeout(long timeOut) {
+ synchronized (this) {
+ if (mLastActivity == null) {
+ try {
+ wait(timeOut);
+ } catch (InterruptedException e) {
+ }
+ }
+ if (mLastActivity == null) {
+ return null;
+ } else {
+ Activity res = mLastActivity;
+ mLastActivity = null;
+ return res;
+ }
+ }
+ }
+
+ /**
+ * Used for intercepting any started activity.
+ *
+ * <p> A non-null return value here will be considered a hit for this monitor.
+ * By default this will return {@code null} and subclasses can override this to return
+ * a non-null value if the intent needs to be intercepted.
+ *
+ * <p> Whenever a new activity is started, this method will be called on instances created
+ * using {@link #Instrumentation.ActivityMonitor()} to check if there is a match. In case
+ * of a match, the activity start will be blocked and the returned result will be used.
+ *
+ * @param intent The intent used for starting the activity.
+ * @return The {@link ActivityResult} that needs to be used in case of a match.
+ */
+ public ActivityResult onStartActivity(Intent intent) {
+ return null;
+ }
+
+ final boolean match(Context who,
+ Activity activity,
+ Intent intent) {
+ if (mIgnoreMatchingSpecificIntents) {
+ return false;
+ }
+ synchronized (this) {
+ if (mWhich != null
+ && mWhich.match(who.getContentResolver(), intent,
+ true, "Instrumentation") < 0) {
+ return false;
+ }
+ if (mClass != null) {
+ String cls = null;
+ if (activity != null) {
+ cls = activity.getClass().getName();
+ } else if (intent.getComponent() != null) {
+ cls = intent.getComponent().getClassName();
+ }
+ if (cls == null || !mClass.equals(cls)) {
+ return false;
+ }
+ }
+ if (activity != null) {
+ mLastActivity = activity;
+ notifyAll();
+ }
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Add a new {@link ActivityMonitor} that will be checked whenever an
+ * activity is started. The monitor is added
+ * after any existing ones; the monitor will be hit only if none of the
+ * existing monitors can themselves handle the Intent.
+ *
+ * @param monitor The new ActivityMonitor to see.
+ *
+ * @see #addMonitor(IntentFilter, ActivityResult, boolean)
+ * @see #checkMonitorHit
+ */
+ public void addMonitor(ActivityMonitor monitor) {
+ synchronized (mSync) {
+ if (mActivityMonitors == null) {
+ mActivityMonitors = new ArrayList();
+ }
+ mActivityMonitors.add(monitor);
+ }
+ }
+
+ /**
+ * A convenience wrapper for {@link #addMonitor(ActivityMonitor)} that
+ * creates an intent filter matching {@link ActivityMonitor} for you and
+ * returns it.
+ *
+ * @param filter The set of intents this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @return The newly created and added activity monitor.
+ *
+ * @see #addMonitor(ActivityMonitor)
+ * @see #checkMonitorHit
+ */
+ public ActivityMonitor addMonitor(
+ IntentFilter filter, ActivityResult result, boolean block) {
+ ActivityMonitor am = new ActivityMonitor(filter, result, block);
+ addMonitor(am);
+ return am;
+ }
+
+ /**
+ * A convenience wrapper for {@link #addMonitor(ActivityMonitor)} that
+ * creates a class matching {@link ActivityMonitor} for you and returns it.
+ *
+ * @param cls The activity class this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @return The newly created and added activity monitor.
+ *
+ * @see #addMonitor(ActivityMonitor)
+ * @see #checkMonitorHit
+ */
+ public ActivityMonitor addMonitor(
+ String cls, ActivityResult result, boolean block) {
+ ActivityMonitor am = new ActivityMonitor(cls, result, block);
+ addMonitor(am);
+ return am;
+ }
+
+ /**
+ * Test whether an existing {@link ActivityMonitor} has been hit. If the
+ * monitor has been hit at least <var>minHits</var> times, then it will be
+ * removed from the activity monitor list and true returned. Otherwise it
+ * is left as-is and false is returned.
+ *
+ * @param monitor The ActivityMonitor to check.
+ * @param minHits The minimum number of hits required.
+ *
+ * @return True if the hit count has been reached, else false.
+ *
+ * @see #addMonitor
+ */
+ public boolean checkMonitorHit(ActivityMonitor monitor, int minHits) {
+ waitForIdleSync();
+ synchronized (mSync) {
+ if (monitor.getHits() < minHits) {
+ return false;
+ }
+ mActivityMonitors.remove(monitor);
+ }
+ return true;
+ }
+
+ /**
+ * Wait for an existing {@link ActivityMonitor} to be hit. Once the
+ * monitor has been hit, it is removed from the activity monitor list and
+ * the first created Activity object that matched it is returned.
+ *
+ * @param monitor The ActivityMonitor to wait for.
+ *
+ * @return The Activity object that matched the monitor.
+ */
+ public Activity waitForMonitor(ActivityMonitor monitor) {
+ Activity activity = monitor.waitForActivity();
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ return activity;
+ }
+
+ /**
+ * Wait for an existing {@link ActivityMonitor} to be hit till the timeout
+ * expires. Once the monitor has been hit, it is removed from the activity
+ * monitor list and the first created Activity object that matched it is
+ * returned. If the timeout expires, a null object is returned.
+ *
+ * @param monitor The ActivityMonitor to wait for.
+ * @param timeOut The timeout value in milliseconds.
+ *
+ * @return The Activity object that matched the monitor.
+ */
+ public Activity waitForMonitorWithTimeout(ActivityMonitor monitor, long timeOut) {
+ Activity activity = monitor.waitForActivityWithTimeout(timeOut);
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ return activity;
+ }
+
+ /**
+ * Remove an {@link ActivityMonitor} that was previously added with
+ * {@link #addMonitor}.
+ *
+ * @param monitor The monitor to remove.
+ *
+ * @see #addMonitor
+ */
+ public void removeMonitor(ActivityMonitor monitor) {
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ }
+
+ /**
+ * Execute a particular menu item.
+ *
+ * @param targetActivity The activity in question.
+ * @param id The identifier associated with the menu item.
+ * @param flag Additional flags, if any.
+ * @return Whether the invocation was successful (for example, it could be
+ * false if item is disabled).
+ */
+ public boolean invokeMenuActionSync(Activity targetActivity,
+ int id, int flag) {
+ class MenuRunnable implements Runnable {
+ private final Activity activity;
+ private final int identifier;
+ private final int flags;
+ boolean returnValue;
+
+ public MenuRunnable(Activity _activity, int _identifier,
+ int _flags) {
+ activity = _activity;
+ identifier = _identifier;
+ flags = _flags;
+ }
+
+ public void run() {
+ Window win = activity.getWindow();
+
+ returnValue = win.performPanelIdentifierAction(
+ Window.FEATURE_OPTIONS_PANEL,
+ identifier,
+ flags);
+ }
+
+ }
+ MenuRunnable mr = new MenuRunnable(targetActivity, id, flag);
+ runOnMainSync(mr);
+ return mr.returnValue;
+ }
+
+ /**
+ * Show the context menu for the currently focused view and executes a
+ * particular context menu item.
+ *
+ * @param targetActivity The activity in question.
+ * @param id The identifier associated with the context menu item.
+ * @param flag Additional flags, if any.
+ * @return Whether the invocation was successful (for example, it could be
+ * false if item is disabled).
+ */
+ public boolean invokeContextMenuAction(Activity targetActivity, int id, int flag) {
+ validateNotAppThread();
+
+ // Bring up context menu for current focus.
+ // It'd be nice to do this through code, but currently ListView depends on
+ // long press to set metadata for its selected child
+
+ final KeyEvent downEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
+ sendKeySync(downEvent);
+
+ // Need to wait for long press
+ waitForIdleSync();
+ try {
+ Thread.sleep(ViewConfiguration.getLongPressTimeout());
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Could not sleep for long press timeout", e);
+ return false;
+ }
+
+ final KeyEvent upEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
+ sendKeySync(upEvent);
+
+ // Wait for context menu to appear
+ waitForIdleSync();
+
+ class ContextMenuRunnable implements Runnable {
+ private final Activity activity;
+ private final int identifier;
+ private final int flags;
+ boolean returnValue;
+
+ public ContextMenuRunnable(Activity _activity, int _identifier,
+ int _flags) {
+ activity = _activity;
+ identifier = _identifier;
+ flags = _flags;
+ }
+
+ public void run() {
+ Window win = activity.getWindow();
+ returnValue = win.performContextMenuIdentifierAction(
+ identifier,
+ flags);
+ }
+
+ }
+
+ ContextMenuRunnable cmr = new ContextMenuRunnable(targetActivity, id, flag);
+ runOnMainSync(cmr);
+ return cmr.returnValue;
+ }
+
+ /**
+ * Sends the key events corresponding to the text to the app being
+ * instrumented.
+ *
+ * @param text The text to be sent.
+ */
+ public void sendStringSync(String text) {
+ if (text == null) {
+ return;
+ }
+ KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+
+ KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray());
+
+ if (events != null) {
+ for (int i = 0; i < events.length; i++) {
+ // We have to change the time of an event before injecting it because
+ // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+ // time stamp and the system rejects too old events. Hence, it is
+ // possible for an event to become stale before it is injected if it
+ // takes too long to inject the preceding ones.
+ sendKeySync(KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(), 0));
+ }
+ }
+ }
+
+ /**
+ * Send a key event to the currently focused window/view and wait for it to
+ * be processed. Finished at some point after the recipient has returned
+ * from its event processing, though it may <em>not</em> have completely
+ * finished reacting from the event -- for example, if it needs to update
+ * its display as a result, it may still be in the process of doing that.
+ *
+ * @param event The event to send to the current focus.
+ */
+ public void sendKeySync(KeyEvent event) {
+ validateNotAppThread();
+
+ long downTime = event.getDownTime();
+ long eventTime = event.getEventTime();
+ int action = event.getAction();
+ int code = event.getKeyCode();
+ int repeatCount = event.getRepeatCount();
+ int metaState = event.getMetaState();
+ int deviceId = event.getDeviceId();
+ int scancode = event.getScanCode();
+ int source = event.getSource();
+ int flags = event.getFlags();
+ if (source == InputDevice.SOURCE_UNKNOWN) {
+ source = InputDevice.SOURCE_KEYBOARD;
+ }
+ if (eventTime == 0) {
+ eventTime = SystemClock.uptimeMillis();
+ }
+ if (downTime == 0) {
+ downTime = eventTime;
+ }
+ KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount, metaState,
+ deviceId, scancode, flags | KeyEvent.FLAG_FROM_SYSTEM, source);
+ InputManager.getInstance().injectInputEvent(newEvent,
+ InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+ }
+
+ /**
+ * Sends an up and down key event sync to the currently focused window.
+ *
+ * @param key The integer keycode for the event.
+ */
+ public void sendKeyDownUpSync(int key) {
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
+ }
+
+ /**
+ * Higher-level method for sending both the down and up key events for a
+ * particular character key code. Equivalent to creating both KeyEvent
+ * objects by hand and calling {@link #sendKeySync}. The event appears
+ * as if it came from keyboard 0, the built in one.
+ *
+ * @param keyCode The key code of the character to send.
+ */
+ public void sendCharacterSync(int keyCode) {
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+ }
+
+ /**
+ * Dispatch a pointer event. Finished at some point after the recipient has
+ * returned from its event processing, though it may <em>not</em> have
+ * completely finished reacting from the event -- for example, if it needs
+ * to update its display as a result, it may still be in the process of
+ * doing that.
+ *
+ * @param event A motion event describing the pointer action. (As noted in
+ * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
+ * {@link SystemClock#uptimeMillis()} as the timebase.
+ */
+ public void sendPointerSync(MotionEvent event) {
+ validateNotAppThread();
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) {
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ }
+ InputManager.getInstance().injectInputEvent(event,
+ InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+ }
+
+ /**
+ * Dispatch a trackball event. Finished at some point after the recipient has
+ * returned from its event processing, though it may <em>not</em> have
+ * completely finished reacting from the event -- for example, if it needs
+ * to update its display as a result, it may still be in the process of
+ * doing that.
+ *
+ * @param event A motion event describing the trackball action. (As noted in
+ * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
+ * {@link SystemClock#uptimeMillis()} as the timebase.
+ */
+ public void sendTrackballEventSync(MotionEvent event) {
+ validateNotAppThread();
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) {
+ event.setSource(InputDevice.SOURCE_TRACKBALL);
+ }
+ InputManager.getInstance().injectInputEvent(event,
+ InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Application} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param cl The ClassLoader with which to instantiate the object.
+ * @param className The name of the class implementing the Application
+ * object.
+ * @param context The context to initialize the application with
+ *
+ * @return The newly instantiated Application object.
+ */
+ public Application newApplication(ClassLoader cl, String className, Context context)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ return newApplication(cl.loadClass(className), context);
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Application} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param clazz The class used to create an Application object from.
+ * @param context The context to initialize the application with
+ *
+ * @return The newly instantiated Application object.
+ */
+ static public Application newApplication(Class<?> clazz, Context context)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ Application app = (Application)clazz.newInstance();
+ app.attach(context);
+ return app;
+ }
+
+ /**
+ * Perform calling of the application's {@link Application#onCreate}
+ * method. The default implementation simply calls through to that method.
+ *
+ * <p>Note: This method will be called immediately after {@link #onCreate(Bundle)}.
+ * Often instrumentation tests start their test thread in onCreate(); you
+ * need to be careful of races between these. (Well between it and
+ * everything else, but let's start here.)
+ *
+ * @param app The application being created.
+ */
+ public void callApplicationOnCreate(Application app) {
+ app.onCreate();
+ }
+
+ /**
+ * Perform instantiation of an {@link Activity} object. This method is intended for use with
+ * unit tests, such as android.test.ActivityUnitTestCase. The activity will be useable
+ * locally but will be missing some of the linkages necessary for use within the system.
+ *
+ * @param clazz The Class of the desired Activity
+ * @param context The base context for the activity to use
+ * @param token The token for this activity to communicate with
+ * @param application The application object (if any)
+ * @param intent The intent that started this Activity
+ * @param info ActivityInfo from the manifest
+ * @param title The title, typically retrieved from the ActivityInfo record
+ * @param parent The parent Activity (if any)
+ * @param id The embedded Id (if any)
+ * @param lastNonConfigurationInstance Arbitrary object that will be
+ * available via {@link Activity#getLastNonConfigurationInstance()
+ * Activity.getLastNonConfigurationInstance()}.
+ * @return Returns the instantiated activity
+ * @throws InstantiationException
+ * @throws IllegalAccessException
+ */
+ public Activity newActivity(Class<?> clazz, Context context,
+ IBinder token, Application application, Intent intent, ActivityInfo info,
+ CharSequence title, Activity parent, String id,
+ Object lastNonConfigurationInstance) throws InstantiationException,
+ IllegalAccessException {
+ Activity activity = (Activity)clazz.newInstance();
+ ActivityThread aThread = null;
+ activity.attach(context, aThread, this, token, 0 /* ident */, application, intent,
+ info, title, parent, id,
+ (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
+ new Configuration(), null /* referrer */, null /* voiceInteractor */,
+ null /* window */, null /* activityConfigCallback */);
+ return activity;
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Activity} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param cl The ClassLoader with which to instantiate the object.
+ * @param className The name of the class implementing the Activity
+ * object.
+ * @param intent The Intent object that specified the activity class being
+ * instantiated.
+ *
+ * @return The newly instantiated Activity object.
+ */
+ public Activity newActivity(ClassLoader cl, String className,
+ Intent intent)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ return (Activity)cl.loadClass(className).newInstance();
+ }
+
+ private void prePerformCreate(Activity activity) {
+ if (mWaitingActivities != null) {
+ synchronized (mSync) {
+ final int N = mWaitingActivities.size();
+ for (int i=0; i<N; i++) {
+ final ActivityWaiter aw = mWaitingActivities.get(i);
+ final Intent intent = aw.intent;
+ if (intent.filterEquals(activity.getIntent())) {
+ aw.activity = activity;
+ mMessageQueue.addIdleHandler(new ActivityGoing(aw));
+ }
+ }
+ }
+ }
+ }
+
+ private void postPerformCreate(Activity activity) {
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ am.match(activity, activity, activity.getIntent());
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onCreate}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to onCreate().
+ */
+ public void callActivityOnCreate(Activity activity, Bundle icicle) {
+ prePerformCreate(activity);
+ activity.performCreate(icicle);
+ postPerformCreate(activity);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onCreate}
+ * method. The default implementation simply calls through to that method.
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to
+ * @param persistentState The previously persisted state (or null)
+ */
+ public void callActivityOnCreate(Activity activity, Bundle icicle,
+ PersistableBundle persistentState) {
+ prePerformCreate(activity);
+ activity.performCreate(icicle, persistentState);
+ postPerformCreate(activity);
+ }
+
+ public void callActivityOnDestroy(Activity activity) {
+ // TODO: the following block causes intermittent hangs when using startActivity
+ // temporarily comment out until root cause is fixed (bug 2630683)
+// if (mWaitingActivities != null) {
+// synchronized (mSync) {
+// final int N = mWaitingActivities.size();
+// for (int i=0; i<N; i++) {
+// final ActivityWaiter aw = mWaitingActivities.get(i);
+// final Intent intent = aw.intent;
+// if (intent.filterEquals(activity.getIntent())) {
+// aw.activity = activity;
+// mMessageQueue.addIdleHandler(new ActivityGoing(aw));
+// }
+// }
+// }
+// }
+
+ activity.performDestroy();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onRestoreInstanceState}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being restored.
+ * @param savedInstanceState The previously saved state being restored.
+ */
+ public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState) {
+ activity.performRestoreInstanceState(savedInstanceState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onRestoreInstanceState}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being restored.
+ * @param savedInstanceState The previously saved state being restored.
+ * @param persistentState The previously persisted state (or null)
+ */
+ public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState,
+ PersistableBundle persistentState) {
+ activity.performRestoreInstanceState(savedInstanceState, persistentState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPostCreate} method.
+ * The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to
+ * onPostCreate().
+ */
+ public void callActivityOnPostCreate(Activity activity, Bundle icicle) {
+ activity.onPostCreate(icicle);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPostCreate} method.
+ * The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to
+ * onPostCreate().
+ */
+ public void callActivityOnPostCreate(Activity activity, Bundle icicle,
+ PersistableBundle persistentState) {
+ activity.onPostCreate(icicle, persistentState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onNewIntent}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity receiving a new Intent.
+ * @param intent The new intent being received.
+ */
+ public void callActivityOnNewIntent(Activity activity, Intent intent) {
+ activity.performNewIntent(intent);
+ }
+
+ /**
+ * @hide
+ */
+ public void callActivityOnNewIntent(Activity activity, ReferrerIntent intent) {
+ final String oldReferrer = activity.mReferrer;
+ try {
+ if (intent != null) {
+ activity.mReferrer = intent.mReferrer;
+ }
+ callActivityOnNewIntent(activity, intent != null ? new Intent(intent) : null);
+ } finally {
+ activity.mReferrer = oldReferrer;
+ }
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onStart}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being started.
+ */
+ public void callActivityOnStart(Activity activity) {
+ activity.onStart();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onRestart}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being restarted.
+ */
+ public void callActivityOnRestart(Activity activity) {
+ activity.onRestart();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onResume} method. The
+ * default implementation simply calls through to that method.
+ *
+ * @param activity The activity being resumed.
+ */
+ public void callActivityOnResume(Activity activity) {
+ activity.mResumed = true;
+ activity.onResume();
+
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ am.match(activity, activity, activity.getIntent());
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onStop}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being stopped.
+ */
+ public void callActivityOnStop(Activity activity) {
+ activity.onStop();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onSaveInstanceState}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being saved.
+ * @param outState The bundle to pass to the call.
+ */
+ public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) {
+ activity.performSaveInstanceState(outState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onSaveInstanceState}
+ * method. The default implementation simply calls through to that method.
+ * @param activity The activity being saved.
+ * @param outState The bundle to pass to the call.
+ * @param outPersistentState The persistent bundle to pass to the call.
+ */
+ public void callActivityOnSaveInstanceState(Activity activity, Bundle outState,
+ PersistableBundle outPersistentState) {
+ activity.performSaveInstanceState(outState, outPersistentState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPause} method. The
+ * default implementation simply calls through to that method.
+ *
+ * @param activity The activity being paused.
+ */
+ public void callActivityOnPause(Activity activity) {
+ activity.performPause();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onUserLeaveHint} method.
+ * The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being notified that the user has navigated away
+ */
+ public void callActivityOnUserLeaving(Activity activity) {
+ activity.performUserLeaving();
+ }
+
+ /*
+ * Starts allocation counting. This triggers a gc and resets the counts.
+ *
+ * @deprecated Accurate counting is a burden on the runtime and may be removed.
+ */
+ @Deprecated
+ public void startAllocCounting() {
+ // Before we start trigger a GC and reset the debug counts. Run the
+ // finalizers and another GC before starting and stopping the alloc
+ // counts. This will free up any objects that were just sitting around
+ // waiting for their finalizers to be run.
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+
+ Debug.resetAllCounts();
+
+ // start the counts
+ Debug.startAllocCounting();
+ }
+
+ /*
+ * Stops allocation counting.
+ *
+ * @deprecated Accurate counting is a burden on the runtime and may be removed.
+ */
+ @Deprecated
+ public void stopAllocCounting() {
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+ Debug.stopAllocCounting();
+ }
+
+ /**
+ * If Results already contains Key, it appends Value to the key's ArrayList
+ * associated with the key. If the key doesn't already exist in results, it
+ * adds the key/value pair to results.
+ */
+ private void addValue(String key, int value, Bundle results) {
+ if (results.containsKey(key)) {
+ List<Integer> list = results.getIntegerArrayList(key);
+ if (list != null) {
+ list.add(value);
+ }
+ } else {
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ list.add(value);
+ results.putIntegerArrayList(key, list);
+ }
+ }
+
+ /**
+ * Returns a bundle with the current results from the allocation counting.
+ */
+ public Bundle getAllocCounts() {
+ Bundle results = new Bundle();
+ results.putLong("global_alloc_count", Debug.getGlobalAllocCount());
+ results.putLong("global_alloc_size", Debug.getGlobalAllocSize());
+ results.putLong("global_freed_count", Debug.getGlobalFreedCount());
+ results.putLong("global_freed_size", Debug.getGlobalFreedSize());
+ results.putLong("gc_invocation_count", Debug.getGlobalGcInvocationCount());
+ return results;
+ }
+
+ /**
+ * Returns a bundle with the counts for various binder counts for this process. Currently the only two that are
+ * reported are the number of send and the number of received transactions.
+ */
+ public Bundle getBinderCounts() {
+ Bundle results = new Bundle();
+ results.putLong("sent_transactions", Debug.getBinderSentTransactions());
+ results.putLong("received_transactions", Debug.getBinderReceivedTransactions());
+ return results;
+ }
+
+ /**
+ * Description of a Activity execution result to return to the original
+ * activity.
+ */
+ public static final class ActivityResult {
+ /**
+ * Create a new activity result. See {@link Activity#setResult} for
+ * more information.
+ *
+ * @param resultCode The result code to propagate back to the
+ * originating activity, often RESULT_CANCELED or RESULT_OK
+ * @param resultData The data to propagate back to the originating
+ * activity.
+ */
+ public ActivityResult(int resultCode, Intent resultData) {
+ mResultCode = resultCode;
+ mResultData = resultData;
+ }
+
+ /**
+ * Retrieve the result code contained in this result.
+ */
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Retrieve the data contained in this result.
+ */
+ public Intent getResultData() {
+ return mResultData;
+ }
+
+ private final int mResultCode;
+ private final Intent mResultData;
+ }
+
+ /**
+ * Execute a startActivity call made by the application. The default
+ * implementation takes care of updating any active {@link ActivityMonitor}
+ * objects and dispatches this call to the system activity manager; you can
+ * override this to watch for the application to start an activity, and
+ * modify what happens when it does.
+ *
+ * <p>This method returns an {@link ActivityResult} object, which you can
+ * use when intercepting application calls to avoid performing the start
+ * activity action but still return the result the application is
+ * expecting. To do this, override this method to catch the call to start
+ * activity so that it returns a new ActivityResult containing the results
+ * you would like the application to see, and don't call up to the super
+ * class. Note that an application is only expecting a result if
+ * <var>requestCode</var> is &gt;= 0.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param who The Context from which the activity is being started.
+ * @param contextThread The main thread of the Context from which the activity
+ * is being started.
+ * @param token Internal token identifying to the system who is starting
+ * the activity; may be null.
+ * @param target Which activity is performing the start (and thus receiving
+ * any result); may be null if this call is not being made
+ * from an activity.
+ * @param intent The actual Intent to start.
+ * @param requestCode Identifier for this request's result; less than zero
+ * if the caller is not expecting a result.
+ * @param options Addition options.
+ *
+ * @return To force the return of a particular result, return an
+ * ActivityResult object containing the desired data; otherwise
+ * return null. The default implementation always returns null.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Activity#startActivity(Intent)
+ * @see Activity#startActivityForResult(Intent, int)
+ * @see Activity#startActivityFromChild
+ *
+ * {@hide}
+ */
+ public ActivityResult execStartActivity(
+ Context who, IBinder contextThread, IBinder token, Activity target,
+ Intent intent, int requestCode, Bundle options) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ Uri referrer = target != null ? target.onProvideReferrer() : null;
+ if (referrer != null) {
+ intent.putExtra(Intent.EXTRA_REFERRER, referrer);
+ }
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intent);
+ }
+ if (result != null) {
+ am.mHits++;
+ return result;
+ } else if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(who);
+ int result = ActivityManager.getService()
+ .startActivity(whoThread, who.getBasePackageName(), intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ token, target != null ? target.mEmbeddedID : null,
+ requestCode, 0, null, options);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ return null;
+ }
+
+ /**
+ * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)},
+ * but accepts an array of activities to be started. Note that active
+ * {@link ActivityMonitor} objects only match against the first activity in
+ * the array.
+ *
+ * {@hide}
+ */
+ public void execStartActivities(Context who, IBinder contextThread,
+ IBinder token, Activity target, Intent[] intents, Bundle options) {
+ execStartActivitiesAsUser(who, contextThread, token, target, intents, options,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)},
+ * but accepts an array of activities to be started. Note that active
+ * {@link ActivityMonitor} objects only match against the first activity in
+ * the array.
+ *
+ * {@hide}
+ */
+ public void execStartActivitiesAsUser(Context who, IBinder contextThread,
+ IBinder token, Activity target, Intent[] intents, Bundle options,
+ int userId) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intents[0]);
+ }
+ if (result != null) {
+ am.mHits++;
+ return;
+ } else if (am.match(who, null, intents[0])) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ String[] resolvedTypes = new String[intents.length];
+ for (int i=0; i<intents.length; i++) {
+ intents[i].migrateExtraStreamToClipData();
+ intents[i].prepareToLeaveProcess(who);
+ resolvedTypes[i] = intents[i].resolveTypeIfNeeded(who.getContentResolver());
+ }
+ int result = ActivityManager.getService()
+ .startActivities(whoThread, who.getBasePackageName(), intents, resolvedTypes,
+ token, options, userId);
+ checkStartActivityResult(result, intents[0]);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ }
+
+ /**
+ * Like {@link #execStartActivity(android.content.Context, android.os.IBinder,
+ * android.os.IBinder, String, android.content.Intent, int, android.os.Bundle)},
+ * but for calls from a {#link Fragment}.
+ *
+ * @param who The Context from which the activity is being started.
+ * @param contextThread The main thread of the Context from which the activity
+ * is being started.
+ * @param token Internal token identifying to the system who is starting
+ * the activity; may be null.
+ * @param target Which element is performing the start (and thus receiving
+ * any result).
+ * @param intent The actual Intent to start.
+ * @param requestCode Identifier for this request's result; less than zero
+ * if the caller is not expecting a result.
+ *
+ * @return To force the return of a particular result, return an
+ * ActivityResult object containing the desired data; otherwise
+ * return null. The default implementation always returns null.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Activity#startActivity(Intent)
+ * @see Activity#startActivityForResult(Intent, int)
+ * @see Activity#startActivityFromChild
+ *
+ * {@hide}
+ */
+ public ActivityResult execStartActivity(
+ Context who, IBinder contextThread, IBinder token, String target,
+ Intent intent, int requestCode, Bundle options) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intent);
+ }
+ if (result != null) {
+ am.mHits++;
+ return result;
+ } else if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(who);
+ int result = ActivityManager.getService()
+ .startActivity(whoThread, who.getBasePackageName(), intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ token, target, requestCode, 0, null, options);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ return null;
+ }
+
+ /**
+ * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)},
+ * but for starting as a particular user.
+ *
+ * @param who The Context from which the activity is being started.
+ * @param contextThread The main thread of the Context from which the activity
+ * is being started.
+ * @param token Internal token identifying to the system who is starting
+ * the activity; may be null.
+ * @param target Which fragment is performing the start (and thus receiving
+ * any result).
+ * @param intent The actual Intent to start.
+ * @param requestCode Identifier for this request's result; less than zero
+ * if the caller is not expecting a result.
+ *
+ * @return To force the return of a particular result, return an
+ * ActivityResult object containing the desired data; otherwise
+ * return null. The default implementation always returns null.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Activity#startActivity(Intent)
+ * @see Activity#startActivityForResult(Intent, int)
+ * @see Activity#startActivityFromChild
+ *
+ * {@hide}
+ */
+ public ActivityResult execStartActivity(
+ Context who, IBinder contextThread, IBinder token, String resultWho,
+ Intent intent, int requestCode, Bundle options, UserHandle user) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intent);
+ }
+ if (result != null) {
+ am.mHits++;
+ return result;
+ } else if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(who);
+ int result = ActivityManager.getService()
+ .startActivityAsUser(whoThread, who.getBasePackageName(), intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ token, resultWho,
+ requestCode, 0, null, options, user.getIdentifier());
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ return null;
+ }
+
+ /**
+ * Special version!
+ * @hide
+ */
+ public ActivityResult execStartActivityAsCaller(
+ Context who, IBinder contextThread, IBinder token, Activity target,
+ Intent intent, int requestCode, Bundle options, boolean ignoreTargetSecurity,
+ int userId) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intent);
+ }
+ if (result != null) {
+ am.mHits++;
+ return result;
+ } else if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(who);
+ int result = ActivityManager.getService()
+ .startActivityAsCaller(whoThread, who.getBasePackageName(), intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ token, target != null ? target.mEmbeddedID : null,
+ requestCode, 0, null, options, ignoreTargetSecurity, userId);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ return null;
+ }
+
+ /**
+ * Special version!
+ * @hide
+ */
+ public void execStartActivityFromAppTask(
+ Context who, IBinder contextThread, IAppTask appTask,
+ Intent intent, Bundle options) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ ActivityResult result = null;
+ if (am.ignoreMatchingSpecificIntents()) {
+ result = am.onStartActivity(intent);
+ }
+ if (result != null) {
+ am.mHits++;
+ return;
+ } else if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(who);
+ int result = appTask.startActivity(whoThread.asBinder(), who.getBasePackageName(),
+ intent, intent.resolveTypeIfNeeded(who.getContentResolver()), options);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failure from system", e);
+ }
+ return;
+ }
+
+ /*package*/ final void init(ActivityThread thread,
+ Context instrContext, Context appContext, ComponentName component,
+ IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection) {
+ mThread = thread;
+ mMessageQueue = mThread.getLooper().myQueue();
+ mInstrContext = instrContext;
+ mAppContext = appContext;
+ mComponent = component;
+ mWatcher = watcher;
+ mUiAutomationConnection = uiAutomationConnection;
+ }
+
+ /** @hide */
+ public static void checkStartActivityResult(int res, Object intent) {
+ if (!ActivityManager.isStartResultFatalError(res)) {
+ return;
+ }
+
+ switch (res) {
+ case ActivityManager.START_INTENT_NOT_RESOLVED:
+ case ActivityManager.START_CLASS_NOT_FOUND:
+ if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
+ throw new ActivityNotFoundException(
+ "Unable to find explicit activity class "
+ + ((Intent)intent).getComponent().toShortString()
+ + "; have you declared this activity in your AndroidManifest.xml?");
+ throw new ActivityNotFoundException(
+ "No Activity found to handle " + intent);
+ case ActivityManager.START_PERMISSION_DENIED:
+ throw new SecurityException("Not allowed to start activity "
+ + intent);
+ case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
+ throw new AndroidRuntimeException(
+ "FORWARD_RESULT_FLAG used while also requesting a result");
+ case ActivityManager.START_NOT_ACTIVITY:
+ throw new IllegalArgumentException(
+ "PendingIntent is not an activity");
+ case ActivityManager.START_NOT_VOICE_COMPATIBLE:
+ throw new SecurityException(
+ "Starting under voice control not allowed for: " + intent);
+ case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
+ throw new IllegalStateException(
+ "Session calling startVoiceActivity does not match active session");
+ case ActivityManager.START_VOICE_HIDDEN_SESSION:
+ throw new IllegalStateException(
+ "Cannot start voice activity on a hidden session");
+ case ActivityManager.START_ASSISTANT_NOT_ACTIVE_SESSION:
+ throw new IllegalStateException(
+ "Session calling startAssistantActivity does not match active session");
+ case ActivityManager.START_ASSISTANT_HIDDEN_SESSION:
+ throw new IllegalStateException(
+ "Cannot start assistant activity on a hidden session");
+ case ActivityManager.START_CANCELED:
+ throw new AndroidRuntimeException("Activity could not be started for "
+ + intent);
+ default:
+ throw new AndroidRuntimeException("Unknown error code "
+ + res + " when starting " + intent);
+ }
+ }
+
+ private final void validateNotAppThread() {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ throw new RuntimeException(
+ "This method can not be called from the main application thread");
+ }
+ }
+
+ /**
+ * Gets the {@link UiAutomation} instance with no flags set.
+ * <p>
+ * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation}
+ * work across application boundaries while the APIs exposed by the instrumentation
+ * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will
+ * not allow you to inject the event in an app different from the instrumentation
+ * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)}
+ * will work regardless of the current application.
+ * </p>
+ * <p>
+ * A typical test case should be using either the {@link UiAutomation} or
+ * {@link Instrumentation} APIs. Using both APIs at the same time is not
+ * a mistake by itself but a client has to be aware of the APIs limitations.
+ * </p>
+ * <p>
+ * Equivalent to {@code getUiAutomation(0)}. If a {@link UiAutomation} exists with different
+ * flags, the flags on that instance will be changed, and then it will be returned.
+ * </p>
+ * @return The UI automation instance.
+ *
+ * @see UiAutomation
+ */
+ public UiAutomation getUiAutomation() {
+ return getUiAutomation(0);
+ }
+
+ /**
+ * Gets the {@link UiAutomation} instance with flags set.
+ * <p>
+ * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation}
+ * work across application boundaries while the APIs exposed by the instrumentation
+ * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will
+ * not allow you to inject the event in an app different from the instrumentation
+ * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)}
+ * will work regardless of the current application.
+ * </p>
+ * <p>
+ * A typical test case should be using either the {@link UiAutomation} or
+ * {@link Instrumentation} APIs. Using both APIs at the same time is not
+ * a mistake by itself but a client has to be aware of the APIs limitations.
+ * </p>
+ * <p>
+ * If a {@link UiAutomation} exists with different flags, the flags on that instance will be
+ * changed, and then it will be returned.
+ * </p>
+ *
+ * @param flags The flags to be passed to the UiAutomation, for example
+ * {@link UiAutomation#FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES}.
+ *
+ * @return The UI automation instance.
+ *
+ * @see UiAutomation
+ */
+ public UiAutomation getUiAutomation(@UiAutomationFlags int flags) {
+ boolean mustCreateNewAutomation = (mUiAutomation == null) || (mUiAutomation.isDestroyed());
+
+ if (mUiAutomationConnection != null) {
+ if (!mustCreateNewAutomation && (mUiAutomation.getFlags() == flags)) {
+ return mUiAutomation;
+ }
+ if (mustCreateNewAutomation) {
+ mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
+ mUiAutomationConnection);
+ } else {
+ mUiAutomation.disconnect();
+ }
+ mUiAutomation.connect(flags);
+ return mUiAutomation;
+ }
+ return null;
+ }
+
+ /**
+ * Takes control of the execution of messages on the specified looper until
+ * {@link TestLooperManager#release} is called.
+ */
+ public TestLooperManager acquireLooperManager(Looper looper) {
+ checkInstrumenting("acquireLooperManager");
+ return new TestLooperManager(looper);
+ }
+
+ private final class InstrumentationThread extends Thread {
+ public InstrumentationThread(String name) {
+ super(name);
+ }
+ public void run() {
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception setting priority of instrumentation thread "
+ + Process.myTid(), e);
+ }
+ if (mAutomaticPerformanceSnapshots) {
+ startPerformanceSnapshot();
+ }
+ onStart();
+ }
+ }
+
+ private static final class EmptyRunnable implements Runnable {
+ public void run() {
+ }
+ }
+
+ private static final class SyncRunnable implements Runnable {
+ private final Runnable mTarget;
+ private boolean mComplete;
+
+ public SyncRunnable(Runnable target) {
+ mTarget = target;
+ }
+
+ public void run() {
+ mTarget.run();
+ synchronized (this) {
+ mComplete = true;
+ notifyAll();
+ }
+ }
+
+ public void waitForComplete() {
+ synchronized (this) {
+ while (!mComplete) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ }
+
+ private static final class ActivityWaiter {
+ public final Intent intent;
+ public Activity activity;
+
+ public ActivityWaiter(Intent _intent) {
+ intent = _intent;
+ }
+ }
+
+ private final class ActivityGoing implements MessageQueue.IdleHandler {
+ private final ActivityWaiter mWaiter;
+
+ public ActivityGoing(ActivityWaiter waiter) {
+ mWaiter = waiter;
+ }
+
+ public final boolean queueIdle() {
+ synchronized (mSync) {
+ mWaitingActivities.remove(mWaiter);
+ mSync.notifyAll();
+ }
+ return false;
+ }
+ }
+
+ private static final class Idler implements MessageQueue.IdleHandler {
+ private final Runnable mCallback;
+ private boolean mIdle;
+
+ public Idler(Runnable callback) {
+ mCallback = callback;
+ mIdle = false;
+ }
+
+ public final boolean queueIdle() {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ synchronized (this) {
+ mIdle = true;
+ notifyAll();
+ }
+ return false;
+ }
+
+ public void waitForIdle() {
+ synchronized (this) {
+ while (!mIdle) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/IntentService.java b/android/app/IntentService.java
new file mode 100644
index 00000000..95ec24cd
--- /dev/null
+++ b/android/app/IntentService.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.WorkerThread;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * IntentService is a base class for {@link Service}s that handle asynchronous
+ * requests (expressed as {@link Intent}s) on demand. Clients send requests
+ * through {@link android.content.Context#startService(Intent)} calls; the
+ * service is started as needed, handles each Intent in turn using a worker
+ * thread, and stops itself when it runs out of work.
+ *
+ * <p>This "work queue processor" pattern is commonly used to offload tasks
+ * from an application's main thread. The IntentService class exists to
+ * simplify this pattern and take care of the mechanics. To use it, extend
+ * IntentService and implement {@link #onHandleIntent(Intent)}. IntentService
+ * will receive the Intents, launch a worker thread, and stop the service as
+ * appropriate.
+ *
+ * <p>All requests are handled on a single worker thread -- they may take as
+ * long as necessary (and will not block the application's main loop), but
+ * only one request will be processed at a time.
+ *
+ * <p class="note"><b>Note:</b> IntentService is subject to all the
+ * <a href="/preview/features/background.html">background execution limits</a>
+ * imposed with Android 8.0 (API level 26). In most cases, you are better off
+ * using {@link android.support.v4.app.JobIntentService}, which uses jobs
+ * instead of services when running on Android 8.0 or higher.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a detailed discussion about how to create services, read the
+ * <a href="{@docRoot}guide/components/services.html">Services</a> developer
+ * guide.</p>
+ * </div>
+ *
+ * @see android.support.v4.app.JobIntentService
+ * @see android.os.AsyncTask
+ */
+public abstract class IntentService extends Service {
+ private volatile Looper mServiceLooper;
+ private volatile ServiceHandler mServiceHandler;
+ private String mName;
+ private boolean mRedelivery;
+
+ private final class ServiceHandler extends Handler {
+ public ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ onHandleIntent((Intent)msg.obj);
+ stopSelf(msg.arg1);
+ }
+ }
+
+ /**
+ * Creates an IntentService. Invoked by your subclass's constructor.
+ *
+ * @param name Used to name the worker thread, important only for debugging.
+ */
+ public IntentService(String name) {
+ super();
+ mName = name;
+ }
+
+ /**
+ * Sets intent redelivery preferences. Usually called from the constructor
+ * with your preferred semantics.
+ *
+ * <p>If enabled is true,
+ * {@link #onStartCommand(Intent, int, int)} will return
+ * {@link Service#START_REDELIVER_INTENT}, so if this process dies before
+ * {@link #onHandleIntent(Intent)} returns, the process will be restarted
+ * and the intent redelivered. If multiple Intents have been sent, only
+ * the most recent one is guaranteed to be redelivered.
+ *
+ * <p>If enabled is false (the default),
+ * {@link #onStartCommand(Intent, int, int)} will return
+ * {@link Service#START_NOT_STICKY}, and if the process dies, the Intent
+ * dies along with it.
+ */
+ public void setIntentRedelivery(boolean enabled) {
+ mRedelivery = enabled;
+ }
+
+ @Override
+ public void onCreate() {
+ // TODO: It would be nice to have an option to hold a partial wakelock
+ // during processing, and to have a static startService(Context, Intent)
+ // method that would launch the service & hand off a wakelock.
+
+ super.onCreate();
+ HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
+ thread.start();
+
+ mServiceLooper = thread.getLooper();
+ mServiceHandler = new ServiceHandler(mServiceLooper);
+ }
+
+ @Override
+ public void onStart(@Nullable Intent intent, int startId) {
+ Message msg = mServiceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent;
+ mServiceHandler.sendMessage(msg);
+ }
+
+ /**
+ * You should not override this method for your IntentService. Instead,
+ * override {@link #onHandleIntent}, which the system calls when the IntentService
+ * receives a start request.
+ * @see android.app.Service#onStartCommand
+ */
+ @Override
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ onStart(intent, startId);
+ return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mServiceLooper.quit();
+ }
+
+ /**
+ * Unless you provide binding for your service, you don't need to implement this
+ * method, because the default implementation returns null.
+ * @see android.app.Service#onBind
+ */
+ @Override
+ @Nullable
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ /**
+ * This method is invoked on the worker thread with a request to process.
+ * Only one Intent is processed at a time, but the processing happens on a
+ * worker thread that runs independently from other application logic.
+ * So, if this code takes a long time, it will hold up other requests to
+ * the same IntentService, but it will not hold up anything else.
+ * When all requests have been handled, the IntentService stops itself,
+ * so you should not call {@link #stopSelf}.
+ *
+ * @param intent The value passed to {@link
+ * android.content.Context#startService(Intent)}.
+ * This may be null if the service is being restarted after
+ * its process has gone away; see
+ * {@link android.app.Service#onStartCommand}
+ * for details.
+ */
+ @WorkerThread
+ protected abstract void onHandleIntent(@Nullable Intent intent);
+}
diff --git a/android/app/JobSchedulerImpl.java b/android/app/JobSchedulerImpl.java
new file mode 100644
index 00000000..4ac44f79
--- /dev/null
+++ b/android/app/JobSchedulerImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// in android.app so ContextImpl has package access
+package android.app;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.app.job.IJobScheduler;
+import android.app.job.JobWorkItem;
+import android.content.Intent;
+import android.os.RemoteException;
+
+import java.util.List;
+
+
+/**
+ * Concrete implementation of the JobScheduler interface
+ * @hide
+ */
+public class JobSchedulerImpl extends JobScheduler {
+ IJobScheduler mBinder;
+
+ /* package */ JobSchedulerImpl(IJobScheduler binder) {
+ mBinder = binder;
+ }
+
+ @Override
+ public int schedule(JobInfo job) {
+ try {
+ return mBinder.schedule(job);
+ } catch (RemoteException e) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ }
+
+ @Override
+ public int enqueue(JobInfo job, JobWorkItem work) {
+ try {
+ return mBinder.enqueue(job, work);
+ } catch (RemoteException e) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ }
+
+ @Override
+ public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) {
+ try {
+ return mBinder.scheduleAsPackage(job, packageName, userId, tag);
+ } catch (RemoteException e) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ }
+
+ @Override
+ public void cancel(int jobId) {
+ try {
+ mBinder.cancel(jobId);
+ } catch (RemoteException e) {}
+
+ }
+
+ @Override
+ public void cancelAll() {
+ try {
+ mBinder.cancelAll();
+ } catch (RemoteException e) {}
+
+ }
+
+ @Override
+ public List<JobInfo> getAllPendingJobs() {
+ try {
+ return mBinder.getAllPendingJobs();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public JobInfo getPendingJob(int jobId) {
+ try {
+ return mBinder.getPendingJob(jobId);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+}
diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java
new file mode 100644
index 00000000..76643d60
--- /dev/null
+++ b/android/app/KeyguardManager.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.app.trust.ITrustManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.service.persistentdata.IPersistentDataBlockService;
+import android.util.Log;
+import android.view.IOnKeyguardExitResult;
+import android.view.IWindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.widget.LockPatternUtils;
+
+import java.util.List;
+
+/**
+ * Class that can be used to lock and unlock the keyboard. The
+ * actual class to control the keyboard locking is
+ * {@link android.app.KeyguardManager.KeyguardLock}.
+ */
+@SystemService(Context.KEYGUARD_SERVICE)
+public class KeyguardManager {
+
+ private static final String TAG = "KeyguardManager";
+
+ private final Context mContext;
+ private final IWindowManager mWM;
+ private final IActivityManager mAm;
+ private final ITrustManager mTrustManager;
+
+ /**
+ * Intent used to prompt user for device credentials.
+ * @hide
+ */
+ public static final String ACTION_CONFIRM_DEVICE_CREDENTIAL =
+ "android.app.action.CONFIRM_DEVICE_CREDENTIAL";
+
+ /**
+ * Intent used to prompt user for device credentials.
+ * @hide
+ */
+ public static final String ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER =
+ "android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER";
+
+ /**
+ * Intent used to prompt user for factory reset credentials.
+ * @hide
+ */
+ public static final String ACTION_CONFIRM_FRP_CREDENTIAL =
+ "android.app.action.CONFIRM_FRP_CREDENTIAL";
+
+ /**
+ * A CharSequence dialog title to show to the user when used with a
+ * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}.
+ * @hide
+ */
+ public static final String EXTRA_TITLE = "android.app.extra.TITLE";
+
+ /**
+ * A CharSequence description to show to the user when used with
+ * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}.
+ * @hide
+ */
+ public static final String EXTRA_DESCRIPTION = "android.app.extra.DESCRIPTION";
+
+ /**
+ * A CharSequence description to show to the user on the alternate button when used with
+ * {@link #ACTION_CONFIRM_FRP_CREDENTIAL}.
+ * @hide
+ */
+ public static final String EXTRA_ALTERNATE_BUTTON_LABEL =
+ "android.app.extra.ALTERNATE_BUTTON_LABEL";
+
+ /**
+ * Result code returned by the activity started by
+ * {@link #createConfirmFactoryResetCredentialIntent} indicating that the user clicked the
+ * alternate button.
+ *
+ * @hide
+ */
+ public static final int RESULT_ALTERNATE = 1;
+
+ /**
+ * Get an intent to prompt the user to confirm credentials (pin, pattern or password)
+ * for the current user of the device. The caller is expected to launch this activity using
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} and check for
+ * {@link android.app.Activity#RESULT_OK} if the user successfully completes the challenge.
+ *
+ * @return the intent for launching the activity or null if no password is required.
+ **/
+ public Intent createConfirmDeviceCredentialIntent(CharSequence title, CharSequence description) {
+ if (!isDeviceSecure()) return null;
+ Intent intent = new Intent(ACTION_CONFIRM_DEVICE_CREDENTIAL);
+ intent.putExtra(EXTRA_TITLE, title);
+ intent.putExtra(EXTRA_DESCRIPTION, description);
+
+ // explicitly set the package for security
+ intent.setPackage(getSettingsPackageForIntent(intent));
+ return intent;
+ }
+
+ /**
+ * Get an intent to prompt the user to confirm credentials (pin, pattern or password)
+ * for the given user. The caller is expected to launch this activity using
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} and check for
+ * {@link android.app.Activity#RESULT_OK} if the user successfully completes the challenge.
+ *
+ * @return the intent for launching the activity or null if no password is required.
+ *
+ * @hide
+ */
+ public Intent createConfirmDeviceCredentialIntent(
+ CharSequence title, CharSequence description, int userId) {
+ if (!isDeviceSecure(userId)) return null;
+ Intent intent = new Intent(ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER);
+ intent.putExtra(EXTRA_TITLE, title);
+ intent.putExtra(EXTRA_DESCRIPTION, description);
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+
+ // explicitly set the package for security
+ intent.setPackage(getSettingsPackageForIntent(intent));
+
+ return intent;
+ }
+
+ /**
+ * Get an intent to prompt the user to confirm credentials (pin, pattern or password)
+ * for the previous owner of the device. The caller is expected to launch this activity using
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} and check for
+ * {@link android.app.Activity#RESULT_OK} if the user successfully completes the challenge.
+ *
+ * @param alternateButtonLabel if not empty, a button is provided with the given label. Upon
+ * clicking this button, the activity returns
+ * {@link #RESULT_ALTERNATE}
+ *
+ * @return the intent for launching the activity or null if the credential of the previous
+ * owner can not be verified (e.g. because there was none, or the device does not support
+ * verifying credentials after a factory reset, or device setup has already been completed).
+ *
+ * @hide
+ */
+ public Intent createConfirmFactoryResetCredentialIntent(
+ CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
+ if (!LockPatternUtils.frpCredentialEnabled()) {
+ Log.w(TAG, "Factory reset credentials not supported.");
+ return null;
+ }
+
+ // Cannot verify credential if the device is provisioned
+ if (Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+ Log.e(TAG, "Factory reset credential cannot be verified after provisioning.");
+ return null;
+ }
+
+ // Make sure we have a credential
+ try {
+ IPersistentDataBlockService pdb = IPersistentDataBlockService.Stub.asInterface(
+ ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE));
+ if (pdb == null) {
+ Log.e(TAG, "No persistent data block service");
+ return null;
+ }
+ if (!pdb.hasFrpCredentialHandle()) {
+ Log.i(TAG, "The persistent data block does not have a factory reset credential.");
+ return null;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ Intent intent = new Intent(ACTION_CONFIRM_FRP_CREDENTIAL);
+ intent.putExtra(EXTRA_TITLE, title);
+ intent.putExtra(EXTRA_DESCRIPTION, description);
+ intent.putExtra(EXTRA_ALTERNATE_BUTTON_LABEL, alternateButtonLabel);
+
+ // explicitly set the package for security
+ intent.setPackage(getSettingsPackageForIntent(intent));
+
+ return intent;
+ }
+
+ private String getSettingsPackageForIntent(Intent intent) {
+ List<ResolveInfo> resolveInfos = mContext.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY);
+ for (int i = 0; i < resolveInfos.size(); i++) {
+ return resolveInfos.get(i).activityInfo.packageName;
+ }
+
+ return "com.android.settings";
+ }
+
+ /**
+ * @deprecated Use {@link LayoutParams#FLAG_DISMISS_KEYGUARD}
+ * and/or {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED}
+ * instead; this allows you to seamlessly hide the keyguard as your application
+ * moves in and out of the foreground and does not require that any special
+ * permissions be requested.
+ *
+ * Handle returned by {@link KeyguardManager#newKeyguardLock} that allows
+ * you to disable / reenable the keyguard.
+ */
+ @Deprecated
+ public class KeyguardLock {
+ private final IBinder mToken = new Binder();
+ private final String mTag;
+
+ KeyguardLock(String tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Disable the keyguard from showing. If the keyguard is currently
+ * showing, hide it. The keyguard will be prevented from showing again
+ * until {@link #reenableKeyguard()} is called.
+ *
+ * A good place to call this is from {@link android.app.Activity#onResume()}
+ *
+ * Note: This call has no effect while any {@link android.app.admin.DevicePolicyManager}
+ * is enabled that requires a password.
+ *
+ * @see #reenableKeyguard()
+ */
+ @RequiresPermission(Manifest.permission.DISABLE_KEYGUARD)
+ public void disableKeyguard() {
+ try {
+ mWM.disableKeyguard(mToken, mTag);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Reenable the keyguard. The keyguard will reappear if the previous
+ * call to {@link #disableKeyguard()} caused it to be hidden.
+ *
+ * A good place to call this is from {@link android.app.Activity#onPause()}
+ *
+ * Note: This call has no effect while any {@link android.app.admin.DevicePolicyManager}
+ * is enabled that requires a password.
+ *
+ * @see #disableKeyguard()
+ */
+ @RequiresPermission(Manifest.permission.DISABLE_KEYGUARD)
+ public void reenableKeyguard() {
+ try {
+ mWM.reenableKeyguard(mToken);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ /**
+ * @deprecated Use {@link KeyguardDismissCallback}
+ * Callback passed to {@link KeyguardManager#exitKeyguardSecurely} to notify
+ * caller of result.
+ */
+ @Deprecated
+ public interface OnKeyguardExitResult {
+
+ /**
+ * @param success True if the user was able to authenticate, false if
+ * not.
+ */
+ void onKeyguardExitResult(boolean success);
+ }
+
+ /**
+ * Callback passed to
+ * {@link KeyguardManager#requestDismissKeyguard(Activity, KeyguardDismissCallback)}
+ * to notify caller of result.
+ */
+ public static abstract class KeyguardDismissCallback {
+
+ /**
+ * Called when dismissing Keyguard is currently not feasible, i.e. when Keyguard is not
+ * available, not showing or when the activity requesting the Keyguard dismissal isn't
+ * showing or isn't showing behind Keyguard.
+ */
+ public void onDismissError() { }
+
+ /**
+ * Called when dismissing Keyguard has succeeded and the device is now unlocked.
+ */
+ public void onDismissSucceeded() { }
+
+ /**
+ * Called when dismissing Keyguard has been cancelled, i.e. when the user cancelled the
+ * operation or the bouncer was hidden for some other reason.
+ */
+ public void onDismissCancelled() { }
+ }
+
+ KeyguardManager(Context context) throws ServiceNotFoundException {
+ mContext = context;
+ mWM = WindowManagerGlobal.getWindowManagerService();
+ mAm = ActivityManager.getService();
+ mTrustManager = ITrustManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TRUST_SERVICE));
+ }
+
+ /**
+ * @deprecated Use {@link LayoutParams#FLAG_DISMISS_KEYGUARD}
+ * and/or {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED}
+ * instead; this allows you to seamlessly hide the keyguard as your application
+ * moves in and out of the foreground and does not require that any special
+ * permissions be requested.
+ *
+ * Enables you to lock or unlock the keyboard. Get an instance of this class by
+ * calling {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ * This class is wrapped by {@link android.app.KeyguardManager KeyguardManager}.
+ * @param tag A tag that informally identifies who you are (for debugging who
+ * is disabling he keyguard).
+ *
+ * @return A {@link KeyguardLock} handle to use to disable and reenable the
+ * keyguard.
+ */
+ @Deprecated
+ public KeyguardLock newKeyguardLock(String tag) {
+ return new KeyguardLock(tag);
+ }
+
+ /**
+ * Return whether the keyguard is currently locked.
+ *
+ * @return true if keyguard is locked.
+ */
+ public boolean isKeyguardLocked() {
+ try {
+ return mWM.isKeyguardLocked();
+ } catch (RemoteException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Return whether the keyguard is secured by a PIN, pattern or password or a SIM card
+ * is currently locked.
+ *
+ * <p>See also {@link #isDeviceSecure()} which ignores SIM locked states.
+ *
+ * @return true if a PIN, pattern or password is set or a SIM card is locked.
+ */
+ public boolean isKeyguardSecure() {
+ try {
+ return mWM.isKeyguardSecure();
+ } catch (RemoteException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * If keyguard screen is showing or in restricted key input mode (i.e. in
+ * keyguard password emergency screen). When in such mode, certain keys,
+ * such as the Home key and the right soft keys, don't work.
+ *
+ * @return true if in keyguard restricted input mode.
+ *
+ * @see android.view.WindowManagerPolicy#inKeyguardRestrictedKeyInputMode
+ */
+ public boolean inKeyguardRestrictedInputMode() {
+ try {
+ return mWM.inKeyguardRestrictedInputMode();
+ } catch (RemoteException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether the device is currently locked and requires a PIN, pattern or
+ * password to unlock.
+ *
+ * @return true if unlocking the device currently requires a PIN, pattern or
+ * password.
+ */
+ public boolean isDeviceLocked() {
+ return isDeviceLocked(UserHandle.myUserId());
+ }
+
+ /**
+ * Per-user version of {@link #isDeviceLocked()}.
+ *
+ * @hide
+ */
+ public boolean isDeviceLocked(int userId) {
+ try {
+ return mTrustManager.isDeviceLocked(userId);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether the device is secured with a PIN, pattern or
+ * password.
+ *
+ * <p>See also {@link #isKeyguardSecure} which treats SIM locked states as secure.
+ *
+ * @return true if a PIN, pattern or password was set.
+ */
+ public boolean isDeviceSecure() {
+ return isDeviceSecure(UserHandle.myUserId());
+ }
+
+ /**
+ * Per-user version of {@link #isDeviceSecure()}.
+ *
+ * @hide
+ */
+ public boolean isDeviceSecure(int userId) {
+ try {
+ return mTrustManager.isDeviceSecure(userId);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public void dismissKeyguard(@NonNull Activity activity,
+ @Nullable KeyguardDismissCallback callback, @Nullable Handler handler) {
+ requestDismissKeyguard(activity, callback);
+ }
+
+ /**
+ * If the device is currently locked (see {@link #isKeyguardLocked()}, requests the Keyguard to
+ * be dismissed.
+ * <p>
+ * If the Keyguard is not secure or the device is currently in a trusted state, calling this
+ * method will immediately dismiss the Keyguard without any user interaction.
+ * <p>
+ * If the Keyguard is secure and the device is not in a trusted state, this will bring up the
+ * UI so the user can enter their credentials.
+ * <p>
+ * If the value set for the {@link Activity} attr {@link android.R.attr#turnScreenOn} is true,
+ * the screen will turn on when the keyguard is dismissed.
+ *
+ * @param activity The activity requesting the dismissal. The activity must be either visible
+ * by using {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED} or must be in a state in
+ * which it would be visible if Keyguard would not be hiding it. If that's not
+ * the case, the request will fail immediately and
+ * {@link KeyguardDismissCallback#onDismissError} will be invoked.
+ * @param callback The callback to be called if the request to dismiss Keyguard was successful
+ * or {@code null} if the caller isn't interested in knowing the result. The
+ * callback will not be invoked if the activity was destroyed before the
+ * callback was received.
+ */
+ public void requestDismissKeyguard(@NonNull Activity activity,
+ @Nullable KeyguardDismissCallback callback) {
+ try {
+ mAm.dismissKeyguard(activity.getActivityToken(), new IKeyguardDismissCallback.Stub() {
+ @Override
+ public void onDismissError() throws RemoteException {
+ if (callback != null && !activity.isDestroyed()) {
+ activity.mHandler.post(callback::onDismissError);
+ }
+ }
+
+ @Override
+ public void onDismissSucceeded() throws RemoteException {
+ if (callback != null && !activity.isDestroyed()) {
+ activity.mHandler.post(callback::onDismissSucceeded);
+ }
+ }
+
+ @Override
+ public void onDismissCancelled() throws RemoteException {
+ if (callback != null && !activity.isDestroyed()) {
+ activity.mHandler.post(callback::onDismissCancelled);
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ Log.i(TAG, "Failed to dismiss keyguard: " + e);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link LayoutParams#FLAG_DISMISS_KEYGUARD}
+ * and/or {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED}
+ * instead; this allows you to seamlessly hide the keyguard as your application
+ * moves in and out of the foreground and does not require that any special
+ * permissions be requested.
+ *
+ * Exit the keyguard securely. The use case for this api is that, after
+ * disabling the keyguard, your app, which was granted permission to
+ * disable the keyguard and show a limited amount of information deemed
+ * safe without the user getting past the keyguard, needs to navigate to
+ * something that is not safe to view without getting past the keyguard.
+ *
+ * This will, if the keyguard is secure, bring up the unlock screen of
+ * the keyguard.
+ *
+ * @param callback Let's you know whether the operation was succesful and
+ * it is safe to launch anything that would normally be considered safe
+ * once the user has gotten past the keyguard.
+ */
+ @Deprecated
+ @RequiresPermission(Manifest.permission.DISABLE_KEYGUARD)
+ public void exitKeyguardSecurely(final OnKeyguardExitResult callback) {
+ try {
+ mWM.exitKeyguardSecurely(new IOnKeyguardExitResult.Stub() {
+ public void onKeyguardExitResult(boolean success) throws RemoteException {
+ if (callback != null) {
+ callback.onKeyguardExitResult(success);
+ }
+ }
+ });
+ } catch (RemoteException e) {
+
+ }
+ }
+}
diff --git a/android/app/LauncherActivity.java b/android/app/LauncherActivity.java
new file mode 100644
index 00000000..9ec7f413
--- /dev/null
+++ b/android/app/LauncherActivity.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.View.OnClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Displays a list of all activities which can be performed
+ * for a given intent. Launches when clicked.
+ *
+ */
+public abstract class LauncherActivity extends ListActivity {
+ Intent mIntent;
+ PackageManager mPackageManager;
+ IconResizer mIconResizer;
+
+ /**
+ * An item in the list
+ */
+ public static class ListItem {
+ public ResolveInfo resolveInfo;
+ public CharSequence label;
+ public Drawable icon;
+ public String packageName;
+ public String className;
+ public Bundle extras;
+
+ ListItem(PackageManager pm, ResolveInfo resolveInfo, IconResizer resizer) {
+ this.resolveInfo = resolveInfo;
+ label = resolveInfo.loadLabel(pm);
+ ComponentInfo ci = resolveInfo.activityInfo;
+ if (ci == null) ci = resolveInfo.serviceInfo;
+ if (label == null && ci != null) {
+ label = resolveInfo.activityInfo.name;
+ }
+
+ if (resizer != null) {
+ icon = resizer.createIconThumbnail(resolveInfo.loadIcon(pm));
+ }
+ packageName = ci.applicationInfo.packageName;
+ className = ci.name;
+ }
+
+ public ListItem() {
+ }
+ }
+
+ /**
+ * Adapter which shows the set of activities that can be performed for a given intent.
+ */
+ private class ActivityAdapter extends BaseAdapter implements Filterable {
+ private final Object lock = new Object();
+ private ArrayList<ListItem> mOriginalValues;
+
+ protected final IconResizer mIconResizer;
+ protected final LayoutInflater mInflater;
+
+ protected List<ListItem> mActivitiesList;
+
+ private Filter mFilter;
+ private final boolean mShowIcons;
+
+ public ActivityAdapter(IconResizer resizer) {
+ mIconResizer = resizer;
+ mInflater = (LayoutInflater) LauncherActivity.this.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mShowIcons = onEvaluateShowIcons();
+ mActivitiesList = makeListItems();
+ }
+
+ public Intent intentForPosition(int position) {
+ if (mActivitiesList == null) {
+ return null;
+ }
+
+ Intent intent = new Intent(mIntent);
+ ListItem item = mActivitiesList.get(position);
+ intent.setClassName(item.packageName, item.className);
+ if (item.extras != null) {
+ intent.putExtras(item.extras);
+ }
+ return intent;
+ }
+
+ public ListItem itemForPosition(int position) {
+ if (mActivitiesList == null) {
+ return null;
+ }
+
+ return mActivitiesList.get(position);
+ }
+
+ public int getCount() {
+ return mActivitiesList != null ? mActivitiesList.size() : 0;
+ }
+
+ public Object getItem(int position) {
+ return position;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ if (convertView == null) {
+ view = mInflater.inflate(
+ com.android.internal.R.layout.activity_list_item_2, parent, false);
+ } else {
+ view = convertView;
+ }
+ bindView(view, mActivitiesList.get(position));
+ return view;
+ }
+
+ private void bindView(View view, ListItem item) {
+ TextView text = (TextView) view;
+ text.setText(item.label);
+ if (mShowIcons) {
+ if (item.icon == null) {
+ item.icon = mIconResizer.createIconThumbnail(item.resolveInfo.loadIcon(getPackageManager()));
+ }
+ text.setCompoundDrawablesWithIntrinsicBounds(item.icon, null, null, null);
+ }
+ }
+
+ public Filter getFilter() {
+ if (mFilter == null) {
+ mFilter = new ArrayFilter();
+ }
+ return mFilter;
+ }
+
+ /**
+ * An array filters constrains the content of the array adapter with a prefix. Each
+ * item that does not start with the supplied prefix is removed from the list.
+ */
+ private class ArrayFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence prefix) {
+ FilterResults results = new FilterResults();
+
+ if (mOriginalValues == null) {
+ synchronized (lock) {
+ mOriginalValues = new ArrayList<ListItem>(mActivitiesList);
+ }
+ }
+
+ if (prefix == null || prefix.length() == 0) {
+ synchronized (lock) {
+ ArrayList<ListItem> list = new ArrayList<ListItem>(mOriginalValues);
+ results.values = list;
+ results.count = list.size();
+ }
+ } else {
+ final String prefixString = prefix.toString().toLowerCase();
+
+ ArrayList<ListItem> values = mOriginalValues;
+ int count = values.size();
+
+ ArrayList<ListItem> newValues = new ArrayList<ListItem>(count);
+
+ for (int i = 0; i < count; i++) {
+ ListItem item = values.get(i);
+
+ String[] words = item.label.toString().toLowerCase().split(" ");
+ int wordCount = words.length;
+
+ for (int k = 0; k < wordCount; k++) {
+ final String word = words[k];
+
+ if (word.startsWith(prefixString)) {
+ newValues.add(item);
+ break;
+ }
+ }
+ }
+
+ results.values = newValues;
+ results.count = newValues.size();
+ }
+
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ //noinspection unchecked
+ mActivitiesList = (List<ListItem>) results.values;
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility class to resize icons to match default icon size.
+ */
+ public class IconResizer {
+ // Code is borrowed from com.android.launcher.Utilities.
+ private int mIconWidth = -1;
+ private int mIconHeight = -1;
+
+ private final Rect mOldBounds = new Rect();
+ private Canvas mCanvas = new Canvas();
+
+ public IconResizer() {
+ mCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
+ Paint.FILTER_BITMAP_FLAG));
+
+ final Resources resources = LauncherActivity.this.getResources();
+ mIconWidth = mIconHeight = (int) resources.getDimension(
+ android.R.dimen.app_icon_size);
+ }
+
+ /**
+ * Returns a Drawable representing the thumbnail of the specified Drawable.
+ * The size of the thumbnail is defined by the dimension
+ * android.R.dimen.launcher_application_icon_size.
+ *
+ * This method is not thread-safe and should be invoked on the UI thread only.
+ *
+ * @param icon The icon to get a thumbnail of.
+ *
+ * @return A thumbnail for the specified icon or the icon itself if the
+ * thumbnail could not be created.
+ */
+ public Drawable createIconThumbnail(Drawable icon) {
+ int width = mIconWidth;
+ int height = mIconHeight;
+
+ final int iconWidth = icon.getIntrinsicWidth();
+ final int iconHeight = icon.getIntrinsicHeight();
+
+ if (icon instanceof PaintDrawable) {
+ PaintDrawable painter = (PaintDrawable) icon;
+ painter.setIntrinsicWidth(width);
+ painter.setIntrinsicHeight(height);
+ }
+
+ if (width > 0 && height > 0) {
+ if (width < iconWidth || height < iconHeight) {
+ final float ratio = (float) iconWidth / iconHeight;
+
+ if (iconWidth > iconHeight) {
+ height = (int) (width / ratio);
+ } else if (iconHeight > iconWidth) {
+ width = (int) (height * ratio);
+ }
+
+ final Bitmap.Config c = icon.getOpacity() != PixelFormat.OPAQUE ?
+ Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
+ final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
+ final Canvas canvas = mCanvas;
+ canvas.setBitmap(thumb);
+ // Copy the old bounds to restore them later
+ // If we were to do oldBounds = icon.getBounds(),
+ // the call to setBounds() that follows would
+ // change the same instance and we would lose the
+ // old bounds
+ mOldBounds.set(icon.getBounds());
+ final int x = (mIconWidth - width) / 2;
+ final int y = (mIconHeight - height) / 2;
+ icon.setBounds(x, y, x + width, y + height);
+ icon.draw(canvas);
+ icon.setBounds(mOldBounds);
+ icon = new BitmapDrawable(getResources(), thumb);
+ canvas.setBitmap(null);
+ } else if (iconWidth < width && iconHeight < height) {
+ final Bitmap.Config c = Bitmap.Config.ARGB_8888;
+ final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
+ final Canvas canvas = mCanvas;
+ canvas.setBitmap(thumb);
+ mOldBounds.set(icon.getBounds());
+ final int x = (width - iconWidth) / 2;
+ final int y = (height - iconHeight) / 2;
+ icon.setBounds(x, y, x + iconWidth, y + iconHeight);
+ icon.draw(canvas);
+ icon.setBounds(mOldBounds);
+ icon = new BitmapDrawable(getResources(), thumb);
+ canvas.setBitmap(null);
+ }
+ }
+
+ return icon;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPackageManager = getPackageManager();
+
+ if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setProgressBarIndeterminateVisibility(true);
+ }
+ onSetContentView();
+
+ mIconResizer = new IconResizer();
+
+ mIntent = new Intent(getTargetIntent());
+ mIntent.setComponent(null);
+ mAdapter = new ActivityAdapter(mIconResizer);
+
+ setListAdapter(mAdapter);
+ getListView().setTextFilterEnabled(true);
+
+ updateAlertTitle();
+ updateButtonText();
+
+ if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ setProgressBarIndeterminateVisibility(false);
+ }
+ }
+
+ private void updateAlertTitle() {
+ TextView alertTitle = (TextView) findViewById(com.android.internal.R.id.alertTitle);
+ if (alertTitle != null) {
+ alertTitle.setText(getTitle());
+ }
+ }
+
+ private void updateButtonText() {
+ Button cancelButton = (Button) findViewById(com.android.internal.R.id.button1);
+ if (cancelButton != null) {
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ updateAlertTitle();
+ }
+
+ @Override
+ public void setTitle(int titleId) {
+ super.setTitle(titleId);
+ updateAlertTitle();
+ }
+
+ /**
+ * Override to call setContentView() with your own content view to
+ * customize the list layout.
+ */
+ protected void onSetContentView() {
+ setContentView(com.android.internal.R.layout.activity_list);
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Intent intent = intentForPosition(position);
+ startActivity(intent);
+ }
+
+ /**
+ * Return the actual Intent for a specific position in our
+ * {@link android.widget.ListView}.
+ * @param position The item whose Intent to return
+ */
+ protected Intent intentForPosition(int position) {
+ ActivityAdapter adapter = (ActivityAdapter) mAdapter;
+ return adapter.intentForPosition(position);
+ }
+
+ /**
+ * Return the {@link ListItem} for a specific position in our
+ * {@link android.widget.ListView}.
+ * @param position The item to return
+ */
+ protected ListItem itemForPosition(int position) {
+ ActivityAdapter adapter = (ActivityAdapter) mAdapter;
+ return adapter.itemForPosition(position);
+ }
+
+ /**
+ * Get the base intent to use when running
+ * {@link PackageManager#queryIntentActivities(Intent, int)}.
+ */
+ protected Intent getTargetIntent() {
+ return new Intent();
+ }
+
+ /**
+ * Perform query on package manager for list items. The default
+ * implementation queries for activities.
+ */
+ protected List<ResolveInfo> onQueryPackageManager(Intent queryIntent) {
+ return mPackageManager.queryIntentActivities(queryIntent, /* no flags */ 0);
+ }
+
+ /**
+ * @hide
+ */
+ protected void onSortResultList(List<ResolveInfo> results) {
+ Collections.sort(results, new ResolveInfo.DisplayNameComparator(mPackageManager));
+ }
+
+ /**
+ * Perform the query to determine which results to show and return a list of them.
+ */
+ public List<ListItem> makeListItems() {
+ // Load all matching activities and sort correctly
+ List<ResolveInfo> list = onQueryPackageManager(mIntent);
+ onSortResultList(list);
+
+ ArrayList<ListItem> result = new ArrayList<ListItem>(list.size());
+ int listSize = list.size();
+ for (int i = 0; i < listSize; i++) {
+ ResolveInfo resolveInfo = list.get(i);
+ result.add(new ListItem(mPackageManager, resolveInfo, null));
+ }
+
+ return result;
+ }
+
+ /**
+ * Whether or not to show icons in the list
+ * @hide keeping this private for now, since only Settings needs it
+ * @return true to show icons beside the activity names, false otherwise
+ */
+ protected boolean onEvaluateShowIcons() {
+ return true;
+ }
+}
diff --git a/android/app/ListActivity.java b/android/app/ListActivity.java
new file mode 100644
index 00000000..2162521e
--- /dev/null
+++ b/android/app/ListActivity.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * An activity that displays a list of items by binding to a data source such as
+ * an array or Cursor, and exposes event handlers when the user selects an item.
+ * <p>
+ * ListActivity hosts a {@link android.widget.ListView ListView} object that can
+ * be bound to different data sources, typically either an array or a Cursor
+ * holding query results. Binding, screen layout, and row layout are discussed
+ * in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ListActivity has a default layout that consists of a single, full-screen list
+ * in the center of the screen. However, if you desire, you can customize the
+ * screen layout by setting your own view layout with setContentView() in
+ * onCreate(). To do this, your own view MUST contain a ListView object with the
+ * id "@android:id/list" (or {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your custom view can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:id/empty". Note that when an empty view is present, the list view
+ * will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom screen layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:paddingLeft=&quot;8dp&quot;
+ * android:paddingRight=&quot;8dp&quot;&gt;
+ *
+ * &lt;ListView android:id=&quot;@android:id/list&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@android:id/empty&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * <p>
+ * You can specify the layout of individual rows in the list. You do this by
+ * specifying a layout resource in the ListAdapter object hosted by the activity
+ * (the ListAdapter binds the ListView to the data; more on this later).
+ * <p>
+ * A ListAdapter constructor takes a parameter that specifies a layout resource
+ * for each row. It also has two additional parameters that let you specify
+ * which data field to associate with which object in the row layout resource.
+ * These two parameters are typically parallel arrays.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text1&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text2&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ListActivity's ListView object to data using a class that
+ * implements the {@link android.widget.ListAdapter ListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps),
+ * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor
+ * query results.
+ * </p>
+ * <p>
+ * The following code from a custom ListActivity demonstrates querying the
+ * Contacts provider for all contacts, then binding the Name and Company fields
+ * to a two line row layout in the activity's ListView.
+ * </p>
+ *
+ * <pre>
+ * public class MyListAdapter extends ListActivity {
+ *
+ * &#064;Override
+ * protected void onCreate(Bundle savedInstanceState){
+ * super.onCreate(savedInstanceState);
+ *
+ * // We'll define a custom screen layout here (the one shown above), but
+ * // typically, you could just use the standard ListActivity layout.
+ * setContentView(R.layout.custom_list_activity_view);
+ *
+ * // Query for all people contacts using the {@link android.provider.Contacts.People} convenience class.
+ * // Put a managed wrapper around the retrieved cursor so we don't have to worry about
+ * // requerying or closing it as the activity changes state.
+ * mCursor = this.getContentResolver().query(People.CONTENT_URI, null, null, null, null);
+ * startManagingCursor(mCursor);
+ *
+ * // Now create a new list adapter bound to the cursor.
+ * // SimpleListAdapter is designed for binding to a Cursor.
+ * ListAdapter adapter = new SimpleCursorAdapter(
+ * this, // Context.
+ * android.R.layout.two_line_list_item, // Specify the row template to use (here, two columns bound to the two retrieved cursor
+ * rows).
+ * mCursor, // Pass in the cursor to bind to.
+ * new String[] {People.NAME, People.COMPANY}, // Array of cursor columns to bind to.
+ * new int[] {android.R.id.text1, android.R.id.text2}); // Parallel array of which template objects to bind to those columns.
+ *
+ * // Bind to our new adapter.
+ * setListAdapter(adapter);
+ * }
+ * }
+ * </pre>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ListView
+ */
+public class ListActivity extends Activity {
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ListAdapter mAdapter;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ListView mList;
+
+ private Handler mHandler = new Handler();
+ private boolean mFinishedStart = false;
+
+ private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ /**
+ * This method will be called when an item in the list is selected.
+ * Subclasses should override. Subclasses can call
+ * getListView().getItemAtPosition(position) if they need to access the
+ * data associated with the selected item.
+ *
+ * @param l The ListView where the click happened
+ * @param v The view that was clicked within the ListView
+ * @param position The position of the view in the list
+ * @param id The row id of the item that was clicked
+ */
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Ensures the list view has been created before Activity restores all
+ * of the view states.
+ *
+ *@see Activity#onRestoreInstanceState(Bundle)
+ */
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ ensureList();
+ super.onRestoreInstanceState(state);
+ }
+
+ /**
+ * @see Activity#onDestroy()
+ */
+ @Override
+ protected void onDestroy() {
+ mHandler.removeCallbacks(mRequestFocus);
+ super.onDestroy();
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ View emptyView = findViewById(com.android.internal.R.id.empty);
+ mList = (ListView)findViewById(com.android.internal.R.id.list);
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mList.setEmptyView(emptyView);
+ }
+ mList.setOnItemClickListener(mOnClickListener);
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mHandler.post(mRequestFocus);
+ mFinishedStart = true;
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ListAdapter adapter) {
+ synchronized (this) {
+ ensureList();
+ mAdapter = adapter;
+ mList.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified
+ * position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ mList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ return mList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ return mList.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's list view widget.
+ */
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's ListView.
+ */
+ public ListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ setContentView(com.android.internal.R.layout.list_content_simple);
+
+ }
+
+ private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id)
+ {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ };
+}
diff --git a/android/app/ListFragment.java b/android/app/ListFragment.java
new file mode 100644
index 00000000..0b96d84d
--- /dev/null
+++ b/android/app/ListFragment.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * A fragment that displays a list of items by binding to a data source such as
+ * an array or Cursor, and exposes event handlers when the user selects an item.
+ * <p>
+ * ListFragment hosts a {@link android.widget.ListView ListView} object that can
+ * be bound to different data sources, typically either an array or a Cursor
+ * holding query results. Binding, screen layout, and row layout are discussed
+ * in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ListFragment has a default layout that consists of a single list view.
+ * However, if you desire, you can customize the fragment layout by returning
+ * your own view hierarchy from {@link #onCreateView}.
+ * To do this, your view hierarchy <em>must</em> contain a ListView object with the
+ * id "@android:id/list" (or {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your view hierarchy can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:empty". Note that when an empty view is present, the list view
+ * will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom list layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:paddingLeft=&quot;8dp&quot;
+ * android:paddingRight=&quot;8dp&quot;&gt;
+ *
+ * &lt;ListView android:id=&quot;@id/android:list&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@id/android:empty&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * <p>
+ * You can specify the layout of individual rows in the list. You do this by
+ * specifying a layout resource in the ListAdapter object hosted by the fragment
+ * (the ListAdapter binds the ListView to the data; more on this later).
+ * <p>
+ * A ListAdapter constructor takes a parameter that specifies a layout resource
+ * for each row. It also has two additional parameters that let you specify
+ * which data field to associate with which object in the row layout resource.
+ * These two parameters are typically parallel arrays.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text1&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text2&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ListFragment's ListView object to data using a class that
+ * implements the {@link android.widget.ListAdapter ListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps),
+ * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor
+ * query results.
+ * </p>
+ * <p>
+ * You <b>must</b> use
+ * {@link #setListAdapter(ListAdapter) ListFragment.setListAdapter()} to
+ * associate the list with an adapter. Do not directly call
+ * {@link ListView#setAdapter(ListAdapter) ListView.setAdapter()} or else
+ * important initialization will be skipped.
+ * </p>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ListView
+ */
+public class ListFragment extends Fragment {
+ final private Handler mHandler = new Handler();
+
+ final private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ final private AdapterView.OnItemClickListener mOnClickListener
+ = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ };
+
+ ListAdapter mAdapter;
+ ListView mList;
+ View mEmptyView;
+ TextView mStandardEmptyView;
+ View mProgressContainer;
+ View mListContainer;
+ CharSequence mEmptyText;
+ boolean mListShown;
+
+ public ListFragment() {
+ }
+
+ /**
+ * Provide default implementation to return a simple list view. Subclasses
+ * can override to replace with their own layout. If doing so, the
+ * returned view hierarchy <em>must</em> have a ListView whose id
+ * is {@link android.R.id#list android.R.id.list} and can optionally
+ * have a sibling view id {@link android.R.id#empty android.R.id.empty}
+ * that is to be shown when the list is empty.
+ *
+ * <p>If you are overriding this method with your own custom content,
+ * consider including the standard layout {@link android.R.layout#list_content}
+ * in your layout file, so that you continue to retain all of the standard
+ * behavior of ListFragment. In particular, this is currently the only
+ * way to have the built-in indeterminant progress state be shown.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(com.android.internal.R.layout.list_content,
+ container, false);
+ }
+
+ /**
+ * Attach to list view once the view hierarchy has been created.
+ */
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ensureList();
+ }
+
+ /**
+ * Detach from list view.
+ */
+ @Override
+ public void onDestroyView() {
+ mHandler.removeCallbacks(mRequestFocus);
+ mList = null;
+ mListShown = false;
+ mEmptyView = mProgressContainer = mListContainer = null;
+ mStandardEmptyView = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * This method will be called when an item in the list is selected.
+ * Subclasses should override. Subclasses can call
+ * getListView().getItemAtPosition(position) if they need to access the
+ * data associated with the selected item.
+ *
+ * @param l The ListView where the click happened
+ * @param v The view that was clicked within the ListView
+ * @param position The position of the view in the list
+ * @param id The row id of the item that was clicked
+ */
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ListAdapter adapter) {
+ boolean hadAdapter = mAdapter != null;
+ mAdapter = adapter;
+ if (mList != null) {
+ mList.setAdapter(adapter);
+ if (!mListShown && !hadAdapter) {
+ // The list was hidden, and previously didn't have an
+ // adapter. It is now time to show it.
+ setListShown(true, getView().getWindowToken() != null);
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified
+ * position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ ensureList();
+ mList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ ensureList();
+ return mList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ ensureList();
+ return mList.getSelectedItemId();
+ }
+
+ /**
+ * Get the fragment's list view widget.
+ */
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * The default content for a ListFragment has a TextView that can
+ * be shown when the list is empty. If you would like to have it
+ * shown, call this method to supply the text it should use.
+ */
+ public void setEmptyText(CharSequence text) {
+ ensureList();
+ if (mStandardEmptyView == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ mStandardEmptyView.setText(text);
+ if (mEmptyText == null) {
+ mList.setEmptyView(mStandardEmptyView);
+ }
+ mEmptyText = text;
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminant progress indicator will be shown instead.
+ *
+ * <p>Applications do not normally need to use this themselves. The default
+ * behavior of ListFragment is to start with the list not being shown, only
+ * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}.
+ * If the list at that point had not been shown, when it does get shown
+ * it will be do without the user ever seeing the hidden state.
+ *
+ * @param shown If true, the list view is shown; if false, the progress
+ * indicator. The initial value is true.
+ */
+ public void setListShown(boolean shown) {
+ setListShown(shown, true);
+ }
+
+ /**
+ * Like {@link #setListShown(boolean)}, but no animation is used when
+ * transitioning from the previous state.
+ */
+ public void setListShownNoAnimation(boolean shown) {
+ setListShown(shown, false);
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminant progress indicator will be shown instead.
+ *
+ * @param shown If true, the list view is shown; if false, the progress
+ * indicator. The initial value is true.
+ * @param animate If true, an animation will be used to transition to the
+ * new state.
+ */
+ private void setListShown(boolean shown, boolean animate) {
+ ensureList();
+ if (mProgressContainer == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ if (mListShown == shown) {
+ return;
+ }
+ mListShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getContext(), android.R.anim.fade_out));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getContext(), android.R.anim.fade_in));
+ } else {
+ mProgressContainer.clearAnimation();
+ mListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getContext(), android.R.anim.fade_in));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getContext(), android.R.anim.fade_out));
+ } else {
+ mProgressContainer.clearAnimation();
+ mListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Get the ListAdapter associated with this fragment's ListView.
+ */
+ public ListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ if (root instanceof ListView) {
+ mList = (ListView)root;
+ } else {
+ mStandardEmptyView = (TextView)root.findViewById(
+ com.android.internal.R.id.internalEmpty);
+ if (mStandardEmptyView == null) {
+ mEmptyView = root.findViewById(android.R.id.empty);
+ } else {
+ mStandardEmptyView.setVisibility(View.GONE);
+ }
+ mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer);
+ mListContainer = root.findViewById(com.android.internal.R.id.listContainer);
+ View rawListView = root.findViewById(android.R.id.list);
+ if (!(rawListView instanceof ListView)) {
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a ListView class");
+ }
+ mList = (ListView)rawListView;
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (mEmptyView != null) {
+ mList.setEmptyView(mEmptyView);
+ } else if (mEmptyText != null) {
+ mStandardEmptyView.setText(mEmptyText);
+ mList.setEmptyView(mStandardEmptyView);
+ }
+ }
+ mListShown = true;
+ mList.setOnItemClickListener(mOnClickListener);
+ if (mAdapter != null) {
+ ListAdapter adapter = mAdapter;
+ mAdapter = null;
+ setListAdapter(adapter);
+ } else {
+ // We are starting without an adapter, so assume we won't
+ // have our data right away and start with the progress indicator.
+ if (mProgressContainer != null) {
+ setListShown(false, false);
+ }
+ }
+ mHandler.post(mRequestFocus);
+ }
+}
diff --git a/android/app/LoadedApk.java b/android/app/LoadedApk.java
new file mode 100644
index 00000000..b38be662
--- /dev/null
+++ b/android/app/LoadedApk.java
@@ -0,0 +1,1694 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.split.SplitDependencyLoader;
+import android.content.res.AssetManager;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+
+import com.android.internal.util.ArrayUtils;
+
+import dalvik.system.VMRuntime;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+
+final class IntentReceiverLeaked extends AndroidRuntimeException {
+ public IntentReceiverLeaked(String msg) {
+ super(msg);
+ }
+}
+
+final class ServiceConnectionLeaked extends AndroidRuntimeException {
+ public ServiceConnectionLeaked(String msg) {
+ super(msg);
+ }
+}
+
+/**
+ * Local state maintained about a currently loaded .apk.
+ * @hide
+ */
+public final class LoadedApk {
+ static final String TAG = "LoadedApk";
+ static final boolean DEBUG = false;
+
+ private final ActivityThread mActivityThread;
+ final String mPackageName;
+ private ApplicationInfo mApplicationInfo;
+ private String mAppDir;
+ private String mResDir;
+ private String[] mOverlayDirs;
+ private String mDataDir;
+ private String mLibDir;
+ private File mDataDirFile;
+ private File mDeviceProtectedDataDirFile;
+ private File mCredentialProtectedDataDirFile;
+ private final ClassLoader mBaseClassLoader;
+ private final boolean mSecurityViolation;
+ private final boolean mIncludeCode;
+ private final boolean mRegisterPackage;
+ private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments();
+ /** WARNING: This may change. Don't hold external references to it. */
+ Resources mResources;
+ private ClassLoader mClassLoader;
+ private Application mApplication;
+
+ private String[] mSplitNames;
+ private String[] mSplitAppDirs;
+ private String[] mSplitResDirs;
+ private String[] mSplitClassLoaderNames;
+
+ private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers
+ = new ArrayMap<>();
+ private final ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mUnregisteredReceivers
+ = new ArrayMap<>();
+ private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
+ = new ArrayMap<>();
+ private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices
+ = new ArrayMap<>();
+
+ Application getApplication() {
+ return mApplication;
+ }
+
+ /**
+ * Create information about a new .apk
+ *
+ * NOTE: This constructor is called with ActivityThread's lock held,
+ * so MUST NOT call back out to the activity manager.
+ */
+ public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
+ CompatibilityInfo compatInfo, ClassLoader baseLoader,
+ boolean securityViolation, boolean includeCode, boolean registerPackage) {
+
+ mActivityThread = activityThread;
+ setApplicationInfo(aInfo);
+ mPackageName = aInfo.packageName;
+ mBaseClassLoader = baseLoader;
+ mSecurityViolation = securityViolation;
+ mIncludeCode = includeCode;
+ mRegisterPackage = registerPackage;
+ mDisplayAdjustments.setCompatibilityInfo(compatInfo);
+ }
+
+ private static ApplicationInfo adjustNativeLibraryPaths(ApplicationInfo info) {
+ // If we're dealing with a multi-arch application that has both
+ // 32 and 64 bit shared libraries, we might need to choose the secondary
+ // depending on what the current runtime's instruction set is.
+ if (info.primaryCpuAbi != null && info.secondaryCpuAbi != null) {
+ final String runtimeIsa = VMRuntime.getRuntime().vmInstructionSet();
+
+ // Get the instruction set that the libraries of secondary Abi is supported.
+ // In presence of a native bridge this might be different than the one secondary Abi used.
+ String secondaryIsa = VMRuntime.getInstructionSet(info.secondaryCpuAbi);
+ final String secondaryDexCodeIsa = SystemProperties.get("ro.dalvik.vm.isa." + secondaryIsa);
+ secondaryIsa = secondaryDexCodeIsa.isEmpty() ? secondaryIsa : secondaryDexCodeIsa;
+
+ // If the runtimeIsa is the same as the primary isa, then we do nothing.
+ // Everything will be set up correctly because info.nativeLibraryDir will
+ // correspond to the right ISA.
+ if (runtimeIsa.equals(secondaryIsa)) {
+ final ApplicationInfo modified = new ApplicationInfo(info);
+ modified.nativeLibraryDir = modified.secondaryNativeLibraryDir;
+ modified.primaryCpuAbi = modified.secondaryCpuAbi;
+ return modified;
+ }
+ }
+
+ return info;
+ }
+
+ /**
+ * Create information about the system package.
+ * Must call {@link #installSystemApplicationInfo} later.
+ */
+ LoadedApk(ActivityThread activityThread) {
+ mActivityThread = activityThread;
+ mApplicationInfo = new ApplicationInfo();
+ mApplicationInfo.packageName = "android";
+ mPackageName = "android";
+ mAppDir = null;
+ mResDir = null;
+ mSplitAppDirs = null;
+ mSplitResDirs = null;
+ mSplitClassLoaderNames = null;
+ mOverlayDirs = null;
+ mDataDir = null;
+ mDataDirFile = null;
+ mDeviceProtectedDataDirFile = null;
+ mCredentialProtectedDataDirFile = null;
+ mLibDir = null;
+ mBaseClassLoader = null;
+ mSecurityViolation = false;
+ mIncludeCode = true;
+ mRegisterPackage = false;
+ mClassLoader = ClassLoader.getSystemClassLoader();
+ mResources = Resources.getSystem();
+ }
+
+ /**
+ * Sets application info about the system package.
+ */
+ void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
+ assert info.packageName.equals("android");
+ mApplicationInfo = info;
+ mClassLoader = classLoader;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public ApplicationInfo getApplicationInfo() {
+ return mApplicationInfo;
+ }
+
+ public int getTargetSdkVersion() {
+ return mApplicationInfo.targetSdkVersion;
+ }
+
+ public boolean isSecurityViolation() {
+ return mSecurityViolation;
+ }
+
+ public CompatibilityInfo getCompatibilityInfo() {
+ return mDisplayAdjustments.getCompatibilityInfo();
+ }
+
+ public void setCompatibilityInfo(CompatibilityInfo compatInfo) {
+ mDisplayAdjustments.setCompatibilityInfo(compatInfo);
+ }
+
+ /**
+ * Gets the array of shared libraries that are listed as
+ * used by the given package.
+ *
+ * @param packageName the name of the package (note: not its
+ * file name)
+ * @return null-ok; the array of shared libraries, each one
+ * a fully-qualified path
+ */
+ private static String[] getLibrariesFor(String packageName) {
+ ApplicationInfo ai = null;
+ try {
+ ai = ActivityThread.getPackageManager().getApplicationInfo(packageName,
+ PackageManager.GET_SHARED_LIBRARY_FILES, UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ if (ai == null) {
+ return null;
+ }
+
+ return ai.sharedLibraryFiles;
+ }
+
+ /**
+ * Update the ApplicationInfo for an app. If oldPaths is null, all the paths are considered
+ * new.
+ * @param aInfo The new ApplicationInfo to use for this LoadedApk
+ * @param oldPaths The code paths for the old ApplicationInfo object. null means no paths can
+ * be reused.
+ */
+ public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
+ @Nullable List<String> oldPaths) {
+ setApplicationInfo(aInfo);
+
+ final List<String> newPaths = new ArrayList<>();
+ makePaths(mActivityThread, aInfo, newPaths);
+ final List<String> addedPaths = new ArrayList<>(newPaths.size());
+
+ if (oldPaths != null) {
+ for (String path : newPaths) {
+ final String apkName = path.substring(path.lastIndexOf(File.separator));
+ boolean match = false;
+ for (String oldPath : oldPaths) {
+ final String oldApkName = oldPath.substring(oldPath.lastIndexOf(File.separator));
+ if (apkName.equals(oldApkName)) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ addedPaths.add(path);
+ }
+ }
+ } else {
+ addedPaths.addAll(newPaths);
+ }
+ synchronized (this) {
+ createOrUpdateClassLoaderLocked(addedPaths);
+ if (mResources != null) {
+ final String[] splitPaths;
+ try {
+ splitPaths = getSplitPaths(null);
+ } catch (NameNotFoundException e) {
+ // This should NEVER fail.
+ throw new AssertionError("null split not found");
+ }
+
+ mResources = ResourcesManager.getInstance().getResources(null, mResDir,
+ splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
+ Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
+ getClassLoader());
+ }
+ }
+ }
+
+ private void setApplicationInfo(ApplicationInfo aInfo) {
+ final int myUid = Process.myUid();
+ aInfo = adjustNativeLibraryPaths(aInfo);
+ mApplicationInfo = aInfo;
+ mAppDir = aInfo.sourceDir;
+ mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
+ mOverlayDirs = aInfo.resourceDirs;
+ mDataDir = aInfo.dataDir;
+ mLibDir = aInfo.nativeLibraryDir;
+ mDataDirFile = FileUtils.newFileOrNull(aInfo.dataDir);
+ mDeviceProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.deviceProtectedDataDir);
+ mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.credentialProtectedDataDir);
+
+ mSplitNames = aInfo.splitNames;
+ mSplitAppDirs = aInfo.splitSourceDirs;
+ mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;
+ mSplitClassLoaderNames = aInfo.splitClassLoaderNames;
+
+ if (aInfo.requestsIsolatedSplitLoading() && !ArrayUtils.isEmpty(mSplitNames)) {
+ mSplitLoader = new SplitDependencyLoaderImpl(aInfo.splitDependencies);
+ }
+ }
+
+ public static void makePaths(ActivityThread activityThread,
+ ApplicationInfo aInfo,
+ List<String> outZipPaths) {
+ makePaths(activityThread, false, aInfo, outZipPaths, null);
+ }
+
+ public static void makePaths(ActivityThread activityThread,
+ boolean isBundledApp,
+ ApplicationInfo aInfo,
+ List<String> outZipPaths,
+ List<String> outLibPaths) {
+ final String appDir = aInfo.sourceDir;
+ final String libDir = aInfo.nativeLibraryDir;
+ final String[] sharedLibraries = aInfo.sharedLibraryFiles;
+
+ outZipPaths.clear();
+ outZipPaths.add(appDir);
+
+ // Do not load all available splits if the app requested isolated split loading.
+ if (aInfo.splitSourceDirs != null && !aInfo.requestsIsolatedSplitLoading()) {
+ Collections.addAll(outZipPaths, aInfo.splitSourceDirs);
+ }
+
+ if (outLibPaths != null) {
+ outLibPaths.clear();
+ }
+
+ /*
+ * The following is a bit of a hack to inject
+ * instrumentation into the system: If the app
+ * being started matches one of the instrumentation names,
+ * then we combine both the "instrumentation" and
+ * "instrumented" app into the path, along with the
+ * concatenation of both apps' shared library lists.
+ */
+
+ String[] instrumentationLibs = null;
+ // activityThread will be null when called from the WebView zygote; just assume
+ // no instrumentation applies in this case.
+ if (activityThread != null) {
+ String instrumentationPackageName = activityThread.mInstrumentationPackageName;
+ String instrumentationAppDir = activityThread.mInstrumentationAppDir;
+ String[] instrumentationSplitAppDirs = activityThread.mInstrumentationSplitAppDirs;
+ String instrumentationLibDir = activityThread.mInstrumentationLibDir;
+
+ String instrumentedAppDir = activityThread.mInstrumentedAppDir;
+ String[] instrumentedSplitAppDirs = activityThread.mInstrumentedSplitAppDirs;
+ String instrumentedLibDir = activityThread.mInstrumentedLibDir;
+
+ if (appDir.equals(instrumentationAppDir)
+ || appDir.equals(instrumentedAppDir)) {
+ outZipPaths.clear();
+ outZipPaths.add(instrumentationAppDir);
+
+ // Only add splits if the app did not request isolated split loading.
+ if (!aInfo.requestsIsolatedSplitLoading()) {
+ if (instrumentationSplitAppDirs != null) {
+ Collections.addAll(outZipPaths, instrumentationSplitAppDirs);
+ }
+
+ if (!instrumentationAppDir.equals(instrumentedAppDir)) {
+ outZipPaths.add(instrumentedAppDir);
+ if (instrumentedSplitAppDirs != null) {
+ Collections.addAll(outZipPaths, instrumentedSplitAppDirs);
+ }
+ }
+ }
+
+ if (outLibPaths != null) {
+ outLibPaths.add(instrumentationLibDir);
+ if (!instrumentationLibDir.equals(instrumentedLibDir)) {
+ outLibPaths.add(instrumentedLibDir);
+ }
+ }
+
+ if (!instrumentedAppDir.equals(instrumentationAppDir)) {
+ instrumentationLibs = getLibrariesFor(instrumentationPackageName);
+ }
+ }
+ }
+
+ if (outLibPaths != null) {
+ if (outLibPaths.isEmpty()) {
+ outLibPaths.add(libDir);
+ }
+
+ // Add path to libraries in apk for current abi. Do this now because more entries
+ // will be added to zipPaths that shouldn't be part of the library path.
+ if (aInfo.primaryCpuAbi != null) {
+ // Add fake libs into the library search path if we target prior to N.
+ if (aInfo.targetSdkVersion < Build.VERSION_CODES.N) {
+ outLibPaths.add("/system/fake-libs" +
+ (VMRuntime.is64BitAbi(aInfo.primaryCpuAbi) ? "64" : ""));
+ }
+ for (String apk : outZipPaths) {
+ outLibPaths.add(apk + "!/lib/" + aInfo.primaryCpuAbi);
+ }
+ }
+
+ if (isBundledApp) {
+ // Add path to system libraries to libPaths;
+ // Access to system libs should be limited
+ // to bundled applications; this is why updated
+ // system apps are not included.
+ outLibPaths.add(System.getProperty("java.library.path"));
+ }
+ }
+
+ // Prepend the shared libraries, maintaining their original order where possible.
+ if (sharedLibraries != null) {
+ int index = 0;
+ for (String lib : sharedLibraries) {
+ if (!outZipPaths.contains(lib)) {
+ outZipPaths.add(index, lib);
+ index++;
+ appendApkLibPathIfNeeded(lib, aInfo, outLibPaths);
+ }
+ }
+ }
+
+ if (instrumentationLibs != null) {
+ for (String lib : instrumentationLibs) {
+ if (!outZipPaths.contains(lib)) {
+ outZipPaths.add(0, lib);
+ appendApkLibPathIfNeeded(lib, aInfo, outLibPaths);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method appends a path to the appropriate native library folder of a
+ * library if this library is hosted in an APK. This allows support for native
+ * shared libraries. The library API is determined based on the application
+ * ABI.
+ *
+ * @param path Path to the library.
+ * @param applicationInfo The application depending on the library.
+ * @param outLibPaths List to which to add the native lib path if needed.
+ */
+ private static void appendApkLibPathIfNeeded(@NonNull String path,
+ @NonNull ApplicationInfo applicationInfo, @Nullable List<String> outLibPaths) {
+ // Looking at the suffix is a little hacky but a safe and simple solution.
+ // We will be revisiting code in the next release and clean this up.
+ if (outLibPaths != null && applicationInfo.primaryCpuAbi != null && path.endsWith(".apk")) {
+ if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
+ outLibPaths.add(path + "!/lib/" + applicationInfo.primaryCpuAbi);
+ }
+ }
+ }
+
+ /*
+ * All indices received by the super class should be shifted by 1 when accessing mSplitNames,
+ * etc. The super class assumes the base APK is index 0, while the PackageManager APIs don't
+ * include the base APK in the list of splits.
+ */
+ private class SplitDependencyLoaderImpl extends SplitDependencyLoader<NameNotFoundException> {
+ private final String[][] mCachedResourcePaths;
+ private final ClassLoader[] mCachedClassLoaders;
+
+ SplitDependencyLoaderImpl(@NonNull SparseArray<int[]> dependencies) {
+ super(dependencies);
+ mCachedResourcePaths = new String[mSplitNames.length + 1][];
+ mCachedClassLoaders = new ClassLoader[mSplitNames.length + 1];
+ }
+
+ @Override
+ protected boolean isSplitCached(int splitIdx) {
+ return mCachedClassLoaders[splitIdx] != null;
+ }
+
+ @Override
+ protected void constructSplit(int splitIdx, @NonNull int[] configSplitIndices,
+ int parentSplitIdx) throws NameNotFoundException {
+ final ArrayList<String> splitPaths = new ArrayList<>();
+ if (splitIdx == 0) {
+ createOrUpdateClassLoaderLocked(null);
+ mCachedClassLoaders[0] = mClassLoader;
+
+ // Never add the base resources here, they always get added no matter what.
+ for (int configSplitIdx : configSplitIndices) {
+ splitPaths.add(mSplitResDirs[configSplitIdx - 1]);
+ }
+ mCachedResourcePaths[0] = splitPaths.toArray(new String[splitPaths.size()]);
+ return;
+ }
+
+ // Since we handled the special base case above, parentSplitIdx is always valid.
+ final ClassLoader parent = mCachedClassLoaders[parentSplitIdx];
+ mCachedClassLoaders[splitIdx] = ApplicationLoaders.getDefault().getClassLoader(
+ mSplitAppDirs[splitIdx - 1], getTargetSdkVersion(), false, null, null, parent,
+ mSplitClassLoaderNames[splitIdx - 1]);
+
+ Collections.addAll(splitPaths, mCachedResourcePaths[parentSplitIdx]);
+ splitPaths.add(mSplitResDirs[splitIdx - 1]);
+ for (int configSplitIdx : configSplitIndices) {
+ splitPaths.add(mSplitResDirs[configSplitIdx - 1]);
+ }
+ mCachedResourcePaths[splitIdx] = splitPaths.toArray(new String[splitPaths.size()]);
+ }
+
+ private int ensureSplitLoaded(String splitName) throws NameNotFoundException {
+ int idx = 0;
+ if (splitName != null) {
+ idx = Arrays.binarySearch(mSplitNames, splitName);
+ if (idx < 0) {
+ throw new PackageManager.NameNotFoundException(
+ "Split name '" + splitName + "' is not installed");
+ }
+ idx += 1;
+ }
+ loadDependenciesForSplit(idx);
+ return idx;
+ }
+
+ ClassLoader getClassLoaderForSplit(String splitName) throws NameNotFoundException {
+ return mCachedClassLoaders[ensureSplitLoaded(splitName)];
+ }
+
+ String[] getSplitPathsForSplit(String splitName) throws NameNotFoundException {
+ return mCachedResourcePaths[ensureSplitLoaded(splitName)];
+ }
+ }
+
+ private SplitDependencyLoaderImpl mSplitLoader;
+
+ ClassLoader getSplitClassLoader(String splitName) throws NameNotFoundException {
+ if (mSplitLoader == null) {
+ return mClassLoader;
+ }
+ return mSplitLoader.getClassLoaderForSplit(splitName);
+ }
+
+ String[] getSplitPaths(String splitName) throws NameNotFoundException {
+ if (mSplitLoader == null) {
+ return mSplitResDirs;
+ }
+ return mSplitLoader.getSplitPathsForSplit(splitName);
+ }
+
+ private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
+ if (mPackageName.equals("android")) {
+ // Note: This branch is taken for system server and we don't need to setup
+ // jit profiling support.
+ if (mClassLoader != null) {
+ // nothing to update
+ return;
+ }
+
+ if (mBaseClassLoader != null) {
+ mClassLoader = mBaseClassLoader;
+ } else {
+ mClassLoader = ClassLoader.getSystemClassLoader();
+ }
+
+ return;
+ }
+
+ // Avoid the binder call when the package is the current application package.
+ // The activity manager will perform ensure that dexopt is performed before
+ // spinning up the process.
+ if (!Objects.equals(mPackageName, ActivityThread.currentPackageName()) && mIncludeCode) {
+ try {
+ ActivityThread.getPackageManager().notifyPackageUse(mPackageName,
+ PackageManager.NOTIFY_PACKAGE_USE_CROSS_PACKAGE);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ if (mRegisterPackage) {
+ try {
+ ActivityManager.getService().addPackageDependency(mPackageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ // Lists for the elements of zip/code and native libraries.
+ //
+ // Both lists are usually not empty. We expect on average one APK for the zip component,
+ // but shared libraries and splits are not uncommon. We expect at least three elements
+ // for native libraries (app-based, system, vendor). As such, give both some breathing
+ // space and initialize to a small value (instead of incurring growth code).
+ final List<String> zipPaths = new ArrayList<>(10);
+ final List<String> libPaths = new ArrayList<>(10);
+
+ final boolean isBundledApp = mApplicationInfo.isSystemApp()
+ && !mApplicationInfo.isUpdatedSystemApp();
+
+ makePaths(mActivityThread, isBundledApp, mApplicationInfo, zipPaths, libPaths);
+
+ String libraryPermittedPath = mDataDir;
+ if (isBundledApp) {
+ // This is necessary to grant bundled apps access to
+ // libraries located in subdirectories of /system/lib
+ libraryPermittedPath += File.pathSeparator +
+ System.getProperty("java.library.path");
+ }
+
+ final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
+
+ // If we're not asked to include code, we construct a classloader that has
+ // no code path included. We still need to set up the library search paths
+ // and permitted path because NativeActivity relies on it (it attempts to
+ // call System.loadLibrary() on a classloader from a LoadedApk with
+ // mIncludeCode == false).
+ if (!mIncludeCode) {
+ if (mClassLoader == null) {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+ mClassLoader = ApplicationLoaders.getDefault().getClassLoader(
+ "" /* codePath */, mApplicationInfo.targetSdkVersion, isBundledApp,
+ librarySearchPath, libraryPermittedPath, mBaseClassLoader,
+ null /* classLoaderName */);
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+
+ return;
+ }
+
+ /*
+ * With all the combination done (if necessary, actually create the java class
+ * loader and set up JIT profiling support if necessary.
+ *
+ * In many cases this is a single APK, so try to avoid the StringBuilder in TextUtils.
+ */
+ final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) :
+ TextUtils.join(File.pathSeparator, zipPaths);
+
+ if (DEBUG) Slog.v(ActivityThread.TAG, "Class path: " + zip +
+ ", JNI path: " + librarySearchPath);
+
+ boolean needToSetupJitProfiles = false;
+ if (mClassLoader == null) {
+ // Temporarily disable logging of disk reads on the Looper thread
+ // as this is early and necessary.
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+
+ mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
+ mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
+ libraryPermittedPath, mBaseClassLoader,
+ mApplicationInfo.classLoaderName);
+
+ StrictMode.setThreadPolicy(oldPolicy);
+ // Setup the class loader paths for profiling.
+ needToSetupJitProfiles = true;
+ }
+
+ if (addedPaths != null && addedPaths.size() > 0) {
+ final String add = TextUtils.join(File.pathSeparator, addedPaths);
+ ApplicationLoaders.getDefault().addPath(mClassLoader, add);
+ // Setup the new code paths for profiling.
+ needToSetupJitProfiles = true;
+ }
+
+ // Setup jit profile support.
+ //
+ // It is ok to call this multiple times if the application gets updated with new splits.
+ // The runtime only keeps track of unique code paths and can handle re-registration of
+ // the same code path. There's no need to pass `addedPaths` since any new code paths
+ // are already in `mApplicationInfo`.
+ //
+ // It is NOT ok to call this function from the system_server (for any of the packages it
+ // loads code from) so we explicitly disallow it there.
+ if (needToSetupJitProfiles && !ActivityThread.isSystem()) {
+ setupJitProfileSupport();
+ }
+ }
+
+ public ClassLoader getClassLoader() {
+ synchronized (this) {
+ if (mClassLoader == null) {
+ createOrUpdateClassLoaderLocked(null /*addedPaths*/);
+ }
+ return mClassLoader;
+ }
+ }
+
+ // Keep in sync with installd (frameworks/native/cmds/installd/commands.cpp).
+ private static File getPrimaryProfileFile(String packageName) {
+ File profileDir = Environment.getDataProfilesDePackageDirectory(
+ UserHandle.myUserId(), packageName);
+ return new File(profileDir, "primary.prof");
+ }
+
+ private void setupJitProfileSupport() {
+ if (!SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
+ return;
+ }
+ // Only set up profile support if the loaded apk has the same uid as the
+ // current process.
+ // Currently, we do not support profiling across different apps.
+ // (e.g. application's uid might be different when the code is
+ // loaded by another app via createApplicationContext)
+ if (mApplicationInfo.uid != Process.myUid()) {
+ return;
+ }
+
+ final List<String> codePaths = new ArrayList<>();
+ if ((mApplicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) {
+ codePaths.add(mApplicationInfo.sourceDir);
+ }
+ if (mApplicationInfo.splitSourceDirs != null) {
+ Collections.addAll(codePaths, mApplicationInfo.splitSourceDirs);
+ }
+
+ if (codePaths.isEmpty()) {
+ // If there are no code paths there's no need to setup a profile file and register with
+ // the runtime,
+ return;
+ }
+
+ final File profileFile = getPrimaryProfileFile(mPackageName);
+
+ VMRuntime.registerAppInfo(profileFile.getPath(),
+ codePaths.toArray(new String[codePaths.size()]));
+
+ // Register the app data directory with the reporter. It will
+ // help deciding whether or not a dex file is the primary apk or a
+ // secondary dex.
+ DexLoadReporter.getInstance().registerAppDataDir(mPackageName, mDataDir);
+ }
+
+ /**
+ * Setup value for Thread.getContextClassLoader(). If the
+ * package will not run in in a VM with other packages, we set
+ * the Java context ClassLoader to the
+ * PackageInfo.getClassLoader value. However, if this VM can
+ * contain multiple packages, we intead set the Java context
+ * ClassLoader to a proxy that will warn about the use of Java
+ * context ClassLoaders and then fall through to use the
+ * system ClassLoader.
+ *
+ * <p> Note that this is similar to but not the same as the
+ * android.content.Context.getClassLoader(). While both
+ * context class loaders are typically set to the
+ * PathClassLoader used to load the package archive in the
+ * single application per VM case, a single Android process
+ * may contain several Contexts executing on one thread with
+ * their own logical ClassLoaders while the Java context
+ * ClassLoader is a thread local. This is why in the case when
+ * we have multiple packages per VM we do not set the Java
+ * context ClassLoader to an arbitrary but instead warn the
+ * user to set their own if we detect that they are using a
+ * Java library that expects it to be set.
+ */
+ private void initializeJavaContextClassLoader() {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ android.content.pm.PackageInfo pi;
+ try {
+ pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ if (pi == null) {
+ throw new IllegalStateException("Unable to get package info for "
+ + mPackageName + "; is package not installed?");
+ }
+ /*
+ * Two possible indications that this package could be
+ * sharing its virtual machine with other packages:
+ *
+ * 1.) the sharedUserId attribute is set in the manifest,
+ * indicating a request to share a VM with other
+ * packages with the same sharedUserId.
+ *
+ * 2.) the application element of the manifest has an
+ * attribute specifying a non-default process name,
+ * indicating the desire to run in another packages VM.
+ */
+ boolean sharedUserIdSet = (pi.sharedUserId != null);
+ boolean processNameNotDefault =
+ (pi.applicationInfo != null &&
+ !mPackageName.equals(pi.applicationInfo.processName));
+ boolean sharable = (sharedUserIdSet || processNameNotDefault);
+ ClassLoader contextClassLoader =
+ (sharable)
+ ? new WarningContextClassLoader()
+ : mClassLoader;
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ }
+
+ private static class WarningContextClassLoader extends ClassLoader {
+
+ private static boolean warned = false;
+
+ private void warn(String methodName) {
+ if (warned) {
+ return;
+ }
+ warned = true;
+ Thread.currentThread().setContextClassLoader(getParent());
+ Slog.w(ActivityThread.TAG, "ClassLoader." + methodName + ": " +
+ "The class loader returned by " +
+ "Thread.getContextClassLoader() may fail for processes " +
+ "that host multiple applications. You should explicitly " +
+ "specify a context class loader. For example: " +
+ "Thread.setContextClassLoader(getClass().getClassLoader());");
+ }
+
+ @Override public URL getResource(String resName) {
+ warn("getResource");
+ return getParent().getResource(resName);
+ }
+
+ @Override public Enumeration<URL> getResources(String resName) throws IOException {
+ warn("getResources");
+ return getParent().getResources(resName);
+ }
+
+ @Override public InputStream getResourceAsStream(String resName) {
+ warn("getResourceAsStream");
+ return getParent().getResourceAsStream(resName);
+ }
+
+ @Override public Class<?> loadClass(String className) throws ClassNotFoundException {
+ warn("loadClass");
+ return getParent().loadClass(className);
+ }
+
+ @Override public void setClassAssertionStatus(String cname, boolean enable) {
+ warn("setClassAssertionStatus");
+ getParent().setClassAssertionStatus(cname, enable);
+ }
+
+ @Override public void setPackageAssertionStatus(String pname, boolean enable) {
+ warn("setPackageAssertionStatus");
+ getParent().setPackageAssertionStatus(pname, enable);
+ }
+
+ @Override public void setDefaultAssertionStatus(boolean enable) {
+ warn("setDefaultAssertionStatus");
+ getParent().setDefaultAssertionStatus(enable);
+ }
+
+ @Override public void clearAssertionStatus() {
+ warn("clearAssertionStatus");
+ getParent().clearAssertionStatus();
+ }
+ }
+
+ public String getAppDir() {
+ return mAppDir;
+ }
+
+ public String getLibDir() {
+ return mLibDir;
+ }
+
+ public String getResDir() {
+ return mResDir;
+ }
+
+ public String[] getSplitAppDirs() {
+ return mSplitAppDirs;
+ }
+
+ public String[] getSplitResDirs() {
+ return mSplitResDirs;
+ }
+
+ public String[] getOverlayDirs() {
+ return mOverlayDirs;
+ }
+
+ public String getDataDir() {
+ return mDataDir;
+ }
+
+ public File getDataDirFile() {
+ return mDataDirFile;
+ }
+
+ public File getDeviceProtectedDataDirFile() {
+ return mDeviceProtectedDataDirFile;
+ }
+
+ public File getCredentialProtectedDataDirFile() {
+ return mCredentialProtectedDataDirFile;
+ }
+
+ public AssetManager getAssets() {
+ return getResources().getAssets();
+ }
+
+ public Resources getResources() {
+ if (mResources == null) {
+ final String[] splitPaths;
+ try {
+ splitPaths = getSplitPaths(null);
+ } catch (NameNotFoundException e) {
+ // This should never fail.
+ throw new AssertionError("null split not found");
+ }
+
+ mResources = ResourcesManager.getInstance().getResources(null, mResDir,
+ splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
+ Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
+ getClassLoader());
+ }
+ return mResources;
+ }
+
+ public Application makeApplication(boolean forceDefaultAppClass,
+ Instrumentation instrumentation) {
+ if (mApplication != null) {
+ return mApplication;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");
+
+ Application app = null;
+
+ String appClass = mApplicationInfo.className;
+ if (forceDefaultAppClass || (appClass == null)) {
+ appClass = "android.app.Application";
+ }
+
+ try {
+ java.lang.ClassLoader cl = getClassLoader();
+ if (!mPackageName.equals("android")) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "initializeJavaContextClassLoader");
+ initializeJavaContextClassLoader();
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
+ app = mActivityThread.mInstrumentation.newApplication(
+ cl, appClass, appContext);
+ appContext.setOuterContext(app);
+ } catch (Exception e) {
+ if (!mActivityThread.mInstrumentation.onException(app, e)) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ throw new RuntimeException(
+ "Unable to instantiate application " + appClass
+ + ": " + e.toString(), e);
+ }
+ }
+ mActivityThread.mAllApplications.add(app);
+ mApplication = app;
+
+ if (instrumentation != null) {
+ try {
+ instrumentation.callApplicationOnCreate(app);
+ } catch (Exception e) {
+ if (!instrumentation.onException(app, e)) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ throw new RuntimeException(
+ "Unable to create application " + app.getClass().getName()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ // Rewrite the R 'constants' for all library apks.
+ SparseArray<String> packageIdentifiers = getAssets().getAssignedPackageIdentifiers();
+ final int N = packageIdentifiers.size();
+ for (int i = 0; i < N; i++) {
+ final int id = packageIdentifiers.keyAt(i);
+ if (id == 0x01 || id == 0x7f) {
+ continue;
+ }
+
+ rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id);
+ }
+
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ return app;
+ }
+
+ private void rewriteRValues(ClassLoader cl, String packageName, int id) {
+ final Class<?> rClazz;
+ try {
+ rClazz = cl.loadClass(packageName + ".R");
+ } catch (ClassNotFoundException e) {
+ // This is not necessarily an error, as some packages do not ship with resources
+ // (or they do not need rewriting).
+ Log.i(TAG, "No resource references to update in package " + packageName);
+ return;
+ }
+
+ final Method callback;
+ try {
+ callback = rClazz.getMethod("onResourcesLoaded", int.class);
+ } catch (NoSuchMethodException e) {
+ // No rewriting to be done.
+ return;
+ }
+
+ Throwable cause;
+ try {
+ callback.invoke(null, id);
+ return;
+ } catch (IllegalAccessException e) {
+ cause = e;
+ } catch (InvocationTargetException e) {
+ cause = e.getCause();
+ }
+
+ throw new RuntimeException("Failed to rewrite resource references for " + packageName,
+ cause);
+ }
+
+ public void removeContextRegistrations(Context context,
+ String who, String what) {
+ final boolean reportRegistrationLeaks = StrictMode.vmRegistrationLeaksEnabled();
+ synchronized (mReceivers) {
+ ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> rmap =
+ mReceivers.remove(context);
+ if (rmap != null) {
+ for (int i = 0; i < rmap.size(); i++) {
+ LoadedApk.ReceiverDispatcher rd = rmap.valueAt(i);
+ IntentReceiverLeaked leak = new IntentReceiverLeaked(
+ what + " " + who + " has leaked IntentReceiver "
+ + rd.getIntentReceiver() + " that was " +
+ "originally registered here. Are you missing a " +
+ "call to unregisterReceiver()?");
+ leak.setStackTrace(rd.getLocation().getStackTrace());
+ Slog.e(ActivityThread.TAG, leak.getMessage(), leak);
+ if (reportRegistrationLeaks) {
+ StrictMode.onIntentReceiverLeaked(leak);
+ }
+ try {
+ ActivityManager.getService().unregisterReceiver(
+ rd.getIIntentReceiver());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ mUnregisteredReceivers.remove(context);
+ }
+
+ synchronized (mServices) {
+ //Slog.i(TAG, "Receiver registrations: " + mReceivers);
+ ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> smap =
+ mServices.remove(context);
+ if (smap != null) {
+ for (int i = 0; i < smap.size(); i++) {
+ LoadedApk.ServiceDispatcher sd = smap.valueAt(i);
+ ServiceConnectionLeaked leak = new ServiceConnectionLeaked(
+ what + " " + who + " has leaked ServiceConnection "
+ + sd.getServiceConnection() + " that was originally bound here");
+ leak.setStackTrace(sd.getLocation().getStackTrace());
+ Slog.e(ActivityThread.TAG, leak.getMessage(), leak);
+ if (reportRegistrationLeaks) {
+ StrictMode.onServiceConnectionLeaked(leak);
+ }
+ try {
+ ActivityManager.getService().unbindService(
+ sd.getIServiceConnection());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ sd.doForget();
+ }
+ }
+ mUnboundServices.remove(context);
+ //Slog.i(TAG, "Service registrations: " + mServices);
+ }
+ }
+
+ public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
+ Context context, Handler handler,
+ Instrumentation instrumentation, boolean registered) {
+ synchronized (mReceivers) {
+ LoadedApk.ReceiverDispatcher rd = null;
+ ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;
+ if (registered) {
+ map = mReceivers.get(context);
+ if (map != null) {
+ rd = map.get(r);
+ }
+ }
+ if (rd == null) {
+ rd = new ReceiverDispatcher(r, context, handler,
+ instrumentation, registered);
+ if (registered) {
+ if (map == null) {
+ map = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
+ mReceivers.put(context, map);
+ }
+ map.put(r, rd);
+ }
+ } else {
+ rd.validate(context, handler);
+ }
+ rd.mForgotten = false;
+ return rd.getIIntentReceiver();
+ }
+ }
+
+ public IIntentReceiver forgetReceiverDispatcher(Context context,
+ BroadcastReceiver r) {
+ synchronized (mReceivers) {
+ ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = mReceivers.get(context);
+ LoadedApk.ReceiverDispatcher rd = null;
+ if (map != null) {
+ rd = map.get(r);
+ if (rd != null) {
+ map.remove(r);
+ if (map.size() == 0) {
+ mReceivers.remove(context);
+ }
+ if (r.getDebugUnregister()) {
+ ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> holder
+ = mUnregisteredReceivers.get(context);
+ if (holder == null) {
+ holder = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
+ mUnregisteredReceivers.put(context, holder);
+ }
+ RuntimeException ex = new IllegalArgumentException(
+ "Originally unregistered here:");
+ ex.fillInStackTrace();
+ rd.setUnregisterLocation(ex);
+ holder.put(r, rd);
+ }
+ rd.mForgotten = true;
+ return rd.getIIntentReceiver();
+ }
+ }
+ ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> holder
+ = mUnregisteredReceivers.get(context);
+ if (holder != null) {
+ rd = holder.get(r);
+ if (rd != null) {
+ RuntimeException ex = rd.getUnregisterLocation();
+ throw new IllegalArgumentException(
+ "Unregistering Receiver " + r
+ + " that was already unregistered", ex);
+ }
+ }
+ if (context == null) {
+ throw new IllegalStateException("Unbinding Receiver " + r
+ + " from Context that is no longer in use: " + context);
+ } else {
+ throw new IllegalArgumentException("Receiver not registered: " + r);
+ }
+
+ }
+ }
+
+ static final class ReceiverDispatcher {
+
+ final static class InnerReceiver extends IIntentReceiver.Stub {
+ final WeakReference<LoadedApk.ReceiverDispatcher> mDispatcher;
+ final LoadedApk.ReceiverDispatcher mStrongRef;
+
+ InnerReceiver(LoadedApk.ReceiverDispatcher rd, boolean strong) {
+ mDispatcher = new WeakReference<LoadedApk.ReceiverDispatcher>(rd);
+ mStrongRef = strong ? rd : null;
+ }
+
+ @Override
+ public void performReceive(Intent intent, int resultCode, String data,
+ Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
+ final LoadedApk.ReceiverDispatcher rd;
+ if (intent == null) {
+ Log.wtf(TAG, "Null intent received");
+ rd = null;
+ } else {
+ rd = mDispatcher.get();
+ }
+ if (ActivityThread.DEBUG_BROADCAST) {
+ int seq = intent.getIntExtra("seq", -1);
+ Slog.i(ActivityThread.TAG, "Receiving broadcast " + intent.getAction()
+ + " seq=" + seq + " to " + (rd != null ? rd.mReceiver : null));
+ }
+ if (rd != null) {
+ rd.performReceive(intent, resultCode, data, extras,
+ ordered, sticky, sendingUser);
+ } else {
+ // The activity manager dispatched a broadcast to a registered
+ // receiver in this process, but before it could be delivered the
+ // receiver was unregistered. Acknowledge the broadcast on its
+ // behalf so that the system's broadcast sequence can continue.
+ if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
+ "Finishing broadcast to unregistered receiver");
+ IActivityManager mgr = ActivityManager.getService();
+ try {
+ if (extras != null) {
+ extras.setAllowFds(false);
+ }
+ mgr.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ final IIntentReceiver.Stub mIIntentReceiver;
+ final BroadcastReceiver mReceiver;
+ final Context mContext;
+ final Handler mActivityThread;
+ final Instrumentation mInstrumentation;
+ final boolean mRegistered;
+ final IntentReceiverLeaked mLocation;
+ RuntimeException mUnregisterLocation;
+ boolean mForgotten;
+
+ final class Args extends BroadcastReceiver.PendingResult {
+ private Intent mCurIntent;
+ private final boolean mOrdered;
+ private boolean mDispatched;
+ private Throwable mPreviousRunStacktrace; // To investigate b/37809561. STOPSHIP remove.
+
+ public Args(Intent intent, int resultCode, String resultData, Bundle resultExtras,
+ boolean ordered, boolean sticky, int sendingUser) {
+ super(resultCode, resultData, resultExtras,
+ mRegistered ? TYPE_REGISTERED : TYPE_UNREGISTERED, ordered,
+ sticky, mIIntentReceiver.asBinder(), sendingUser, intent.getFlags());
+ mCurIntent = intent;
+ mOrdered = ordered;
+ }
+
+ public final Runnable getRunnable() {
+ return () -> {
+ final BroadcastReceiver receiver = mReceiver;
+ final boolean ordered = mOrdered;
+
+ if (ActivityThread.DEBUG_BROADCAST) {
+ int seq = mCurIntent.getIntExtra("seq", -1);
+ Slog.i(ActivityThread.TAG, "Dispatching broadcast " + mCurIntent.getAction()
+ + " seq=" + seq + " to " + mReceiver);
+ Slog.i(ActivityThread.TAG, " mRegistered=" + mRegistered
+ + " mOrderedHint=" + ordered);
+ }
+
+ final IActivityManager mgr = ActivityManager.getService();
+ final Intent intent = mCurIntent;
+ if (intent == null) {
+ Log.wtf(TAG, "Null intent being dispatched, mDispatched=" + mDispatched
+ + ": run() previously called at "
+ + Log.getStackTraceString(mPreviousRunStacktrace));
+ }
+
+ mCurIntent = null;
+ mDispatched = true;
+ mPreviousRunStacktrace = new Throwable("Previous stacktrace");
+ if (receiver == null || intent == null || mForgotten) {
+ if (mRegistered && ordered) {
+ if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
+ "Finishing null broadcast to " + mReceiver);
+ sendFinished(mgr);
+ }
+ return;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveReg");
+ try {
+ ClassLoader cl = mReceiver.getClass().getClassLoader();
+ intent.setExtrasClassLoader(cl);
+ intent.prepareToEnterProcess();
+ setExtrasClassLoader(cl);
+ receiver.setPendingResult(this);
+ receiver.onReceive(mContext, intent);
+ } catch (Exception e) {
+ if (mRegistered && ordered) {
+ if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
+ "Finishing failed broadcast to " + mReceiver);
+ sendFinished(mgr);
+ }
+ if (mInstrumentation == null ||
+ !mInstrumentation.onException(mReceiver, e)) {
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ throw new RuntimeException(
+ "Error receiving broadcast " + intent
+ + " in " + mReceiver, e);
+ }
+ }
+
+ if (receiver.getPendingResult() != null) {
+ finish();
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ };
+ }
+ }
+
+ ReceiverDispatcher(BroadcastReceiver receiver, Context context,
+ Handler activityThread, Instrumentation instrumentation,
+ boolean registered) {
+ if (activityThread == null) {
+ throw new NullPointerException("Handler must not be null");
+ }
+
+ mIIntentReceiver = new InnerReceiver(this, !registered);
+ mReceiver = receiver;
+ mContext = context;
+ mActivityThread = activityThread;
+ mInstrumentation = instrumentation;
+ mRegistered = registered;
+ mLocation = new IntentReceiverLeaked(null);
+ mLocation.fillInStackTrace();
+ }
+
+ void validate(Context context, Handler activityThread) {
+ if (mContext != context) {
+ throw new IllegalStateException(
+ "Receiver " + mReceiver +
+ " registered with differing Context (was " +
+ mContext + " now " + context + ")");
+ }
+ if (mActivityThread != activityThread) {
+ throw new IllegalStateException(
+ "Receiver " + mReceiver +
+ " registered with differing handler (was " +
+ mActivityThread + " now " + activityThread + ")");
+ }
+ }
+
+ IntentReceiverLeaked getLocation() {
+ return mLocation;
+ }
+
+ BroadcastReceiver getIntentReceiver() {
+ return mReceiver;
+ }
+
+ IIntentReceiver getIIntentReceiver() {
+ return mIIntentReceiver;
+ }
+
+ void setUnregisterLocation(RuntimeException ex) {
+ mUnregisterLocation = ex;
+ }
+
+ RuntimeException getUnregisterLocation() {
+ return mUnregisterLocation;
+ }
+
+ public void performReceive(Intent intent, int resultCode, String data,
+ Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
+ final Args args = new Args(intent, resultCode, data, extras, ordered,
+ sticky, sendingUser);
+ if (intent == null) {
+ Log.wtf(TAG, "Null intent received");
+ } else {
+ if (ActivityThread.DEBUG_BROADCAST) {
+ int seq = intent.getIntExtra("seq", -1);
+ Slog.i(ActivityThread.TAG, "Enqueueing broadcast " + intent.getAction()
+ + " seq=" + seq + " to " + mReceiver);
+ }
+ }
+ if (intent == null || !mActivityThread.post(args.getRunnable())) {
+ if (mRegistered && ordered) {
+ IActivityManager mgr = ActivityManager.getService();
+ if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
+ "Finishing sync broadcast to " + mReceiver);
+ args.sendFinished(mgr);
+ }
+ }
+ }
+
+ }
+
+ public final IServiceConnection getServiceDispatcher(ServiceConnection c,
+ Context context, Handler handler, int flags) {
+ synchronized (mServices) {
+ LoadedApk.ServiceDispatcher sd = null;
+ ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);
+ if (map != null) {
+ if (DEBUG) Slog.d(TAG, "Returning existing dispatcher " + sd + " for conn " + c);
+ sd = map.get(c);
+ }
+ if (sd == null) {
+ sd = new ServiceDispatcher(c, context, handler, flags);
+ if (DEBUG) Slog.d(TAG, "Creating new dispatcher " + sd + " for conn " + c);
+ if (map == null) {
+ map = new ArrayMap<>();
+ mServices.put(context, map);
+ }
+ map.put(c, sd);
+ } else {
+ sd.validate(context, handler);
+ }
+ return sd.getIServiceConnection();
+ }
+ }
+
+ public final IServiceConnection forgetServiceDispatcher(Context context,
+ ServiceConnection c) {
+ synchronized (mServices) {
+ ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map
+ = mServices.get(context);
+ LoadedApk.ServiceDispatcher sd = null;
+ if (map != null) {
+ sd = map.get(c);
+ if (sd != null) {
+ if (DEBUG) Slog.d(TAG, "Removing dispatcher " + sd + " for conn " + c);
+ map.remove(c);
+ sd.doForget();
+ if (map.size() == 0) {
+ mServices.remove(context);
+ }
+ if ((sd.getFlags()&Context.BIND_DEBUG_UNBIND) != 0) {
+ ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> holder
+ = mUnboundServices.get(context);
+ if (holder == null) {
+ holder = new ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>();
+ mUnboundServices.put(context, holder);
+ }
+ RuntimeException ex = new IllegalArgumentException(
+ "Originally unbound here:");
+ ex.fillInStackTrace();
+ sd.setUnbindLocation(ex);
+ holder.put(c, sd);
+ }
+ return sd.getIServiceConnection();
+ }
+ }
+ ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> holder
+ = mUnboundServices.get(context);
+ if (holder != null) {
+ sd = holder.get(c);
+ if (sd != null) {
+ RuntimeException ex = sd.getUnbindLocation();
+ throw new IllegalArgumentException(
+ "Unbinding Service " + c
+ + " that was already unbound", ex);
+ }
+ }
+ if (context == null) {
+ throw new IllegalStateException("Unbinding Service " + c
+ + " from Context that is no longer in use: " + context);
+ } else {
+ throw new IllegalArgumentException("Service not registered: " + c);
+ }
+ }
+ }
+
+ static final class ServiceDispatcher {
+ private final ServiceDispatcher.InnerConnection mIServiceConnection;
+ private final ServiceConnection mConnection;
+ private final Context mContext;
+ private final Handler mActivityThread;
+ private final ServiceConnectionLeaked mLocation;
+ private final int mFlags;
+
+ private RuntimeException mUnbindLocation;
+
+ private boolean mForgotten;
+
+ private static class ConnectionInfo {
+ IBinder binder;
+ IBinder.DeathRecipient deathMonitor;
+ }
+
+ private static class InnerConnection extends IServiceConnection.Stub {
+ final WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;
+
+ InnerConnection(LoadedApk.ServiceDispatcher sd) {
+ mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);
+ }
+
+ public void connected(ComponentName name, IBinder service, boolean dead)
+ throws RemoteException {
+ LoadedApk.ServiceDispatcher sd = mDispatcher.get();
+ if (sd != null) {
+ sd.connected(name, service, dead);
+ }
+ }
+ }
+
+ private final ArrayMap<ComponentName, ServiceDispatcher.ConnectionInfo> mActiveConnections
+ = new ArrayMap<ComponentName, ServiceDispatcher.ConnectionInfo>();
+
+ ServiceDispatcher(ServiceConnection conn,
+ Context context, Handler activityThread, int flags) {
+ mIServiceConnection = new InnerConnection(this);
+ mConnection = conn;
+ mContext = context;
+ mActivityThread = activityThread;
+ mLocation = new ServiceConnectionLeaked(null);
+ mLocation.fillInStackTrace();
+ mFlags = flags;
+ }
+
+ void validate(Context context, Handler activityThread) {
+ if (mContext != context) {
+ throw new RuntimeException(
+ "ServiceConnection " + mConnection +
+ " registered with differing Context (was " +
+ mContext + " now " + context + ")");
+ }
+ if (mActivityThread != activityThread) {
+ throw new RuntimeException(
+ "ServiceConnection " + mConnection +
+ " registered with differing handler (was " +
+ mActivityThread + " now " + activityThread + ")");
+ }
+ }
+
+ void doForget() {
+ synchronized(this) {
+ for (int i=0; i<mActiveConnections.size(); i++) {
+ ServiceDispatcher.ConnectionInfo ci = mActiveConnections.valueAt(i);
+ ci.binder.unlinkToDeath(ci.deathMonitor, 0);
+ }
+ mActiveConnections.clear();
+ mForgotten = true;
+ }
+ }
+
+ ServiceConnectionLeaked getLocation() {
+ return mLocation;
+ }
+
+ ServiceConnection getServiceConnection() {
+ return mConnection;
+ }
+
+ IServiceConnection getIServiceConnection() {
+ return mIServiceConnection;
+ }
+
+ int getFlags() {
+ return mFlags;
+ }
+
+ void setUnbindLocation(RuntimeException ex) {
+ mUnbindLocation = ex;
+ }
+
+ RuntimeException getUnbindLocation() {
+ return mUnbindLocation;
+ }
+
+ public void connected(ComponentName name, IBinder service, boolean dead) {
+ if (mActivityThread != null) {
+ mActivityThread.post(new RunConnection(name, service, 0, dead));
+ } else {
+ doConnected(name, service, dead);
+ }
+ }
+
+ public void death(ComponentName name, IBinder service) {
+ if (mActivityThread != null) {
+ mActivityThread.post(new RunConnection(name, service, 1, false));
+ } else {
+ doDeath(name, service);
+ }
+ }
+
+ public void doConnected(ComponentName name, IBinder service, boolean dead) {
+ ServiceDispatcher.ConnectionInfo old;
+ ServiceDispatcher.ConnectionInfo info;
+
+ synchronized (this) {
+ if (mForgotten) {
+ // We unbound before receiving the connection; ignore
+ // any connection received.
+ return;
+ }
+ old = mActiveConnections.get(name);
+ if (old != null && old.binder == service) {
+ // Huh, already have this one. Oh well!
+ return;
+ }
+
+ if (service != null) {
+ // A new service is being connected... set it all up.
+ info = new ConnectionInfo();
+ info.binder = service;
+ info.deathMonitor = new DeathMonitor(name, service);
+ try {
+ service.linkToDeath(info.deathMonitor, 0);
+ mActiveConnections.put(name, info);
+ } catch (RemoteException e) {
+ // This service was dead before we got it... just
+ // don't do anything with it.
+ mActiveConnections.remove(name);
+ return;
+ }
+
+ } else {
+ // The named service is being disconnected... clean up.
+ mActiveConnections.remove(name);
+ }
+
+ if (old != null) {
+ old.binder.unlinkToDeath(old.deathMonitor, 0);
+ }
+ }
+
+ // If there was an old service, it is now disconnected.
+ if (old != null) {
+ mConnection.onServiceDisconnected(name);
+ }
+ if (dead) {
+ mConnection.onBindingDied(name);
+ }
+ // If there is a new service, it is now connected.
+ if (service != null) {
+ mConnection.onServiceConnected(name, service);
+ }
+ }
+
+ public void doDeath(ComponentName name, IBinder service) {
+ synchronized (this) {
+ ConnectionInfo old = mActiveConnections.get(name);
+ if (old == null || old.binder != service) {
+ // Death for someone different than who we last
+ // reported... just ignore it.
+ return;
+ }
+ mActiveConnections.remove(name);
+ old.binder.unlinkToDeath(old.deathMonitor, 0);
+ }
+
+ mConnection.onServiceDisconnected(name);
+ }
+
+ private final class RunConnection implements Runnable {
+ RunConnection(ComponentName name, IBinder service, int command, boolean dead) {
+ mName = name;
+ mService = service;
+ mCommand = command;
+ mDead = dead;
+ }
+
+ public void run() {
+ if (mCommand == 0) {
+ doConnected(mName, mService, mDead);
+ } else if (mCommand == 1) {
+ doDeath(mName, mService);
+ }
+ }
+
+ final ComponentName mName;
+ final IBinder mService;
+ final int mCommand;
+ final boolean mDead;
+ }
+
+ private final class DeathMonitor implements IBinder.DeathRecipient
+ {
+ DeathMonitor(ComponentName name, IBinder service) {
+ mName = name;
+ mService = service;
+ }
+
+ public void binderDied() {
+ death(mName, mService);
+ }
+
+ final ComponentName mName;
+ final IBinder mService;
+ }
+ }
+}
diff --git a/android/app/LoaderManager.java b/android/app/LoaderManager.java
new file mode 100644
index 00000000..56dfc589
--- /dev/null
+++ b/android/app/LoaderManager.java
@@ -0,0 +1,902 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Loader;
+import android.os.Bundle;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.Modifier;
+
+/**
+ * Interface associated with an {@link Activity} or {@link Fragment} for managing
+ * one or more {@link android.content.Loader} instances associated with it. This
+ * helps an application manage longer-running operations in conjunction with the
+ * Activity or Fragment lifecycle; the most common use of this is with a
+ * {@link android.content.CursorLoader}, however applications are free to write
+ * their own loaders for loading other types of data.
+ *
+ * While the LoaderManager API was introduced in
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, a version of the API
+ * at is also available for use on older platforms through
+ * {@link android.support.v4.app.FragmentActivity}. See the blog post
+ * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html">
+ * Fragments For All</a> for more details.
+ *
+ * <p>As an example, here is the full implementation of a {@link Fragment}
+ * that displays a {@link android.widget.ListView} containing the results of
+ * a query against the contacts content provider. It uses a
+ * {@link android.content.CursorLoader} to manage the query on the provider.
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LoaderCursor.java
+ * fragment_cursor}
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using loaders, read the
+ * <a href="{@docRoot}guide/topics/fundamentals/loaders.html">Loaders</a> developer guide.</p>
+ * </div>
+ */
+public abstract class LoaderManager {
+ /**
+ * Callback interface for a client to interact with the manager.
+ */
+ public interface LoaderCallbacks<D> {
+ /**
+ * Instantiate and return a new Loader for the given ID.
+ *
+ * @param id The ID whose loader is to be created.
+ * @param args Any arguments supplied by the caller.
+ * @return Return a new Loader instance that is ready to start loading.
+ */
+ public Loader<D> onCreateLoader(int id, Bundle args);
+
+ /**
+ * Called when a previously created loader has finished its load. Note
+ * that normally an application is <em>not</em> allowed to commit fragment
+ * transactions while in this call, since it can happen after an
+ * activity's state is saved. See {@link FragmentManager#beginTransaction()
+ * FragmentManager.openTransaction()} for further discussion on this.
+ *
+ * <p>This function is guaranteed to be called prior to the release of
+ * the last data that was supplied for this Loader. At this point
+ * you should remove all use of the old data (since it will be released
+ * soon), but should not do your own release of the data since its Loader
+ * owns it and will take care of that. The Loader will take care of
+ * management of its data so you don't have to. In particular:
+ *
+ * <ul>
+ * <li> <p>The Loader will monitor for changes to the data, and report
+ * them to you through new calls here. You should not monitor the
+ * data yourself. For example, if the data is a {@link android.database.Cursor}
+ * and you place it in a {@link android.widget.CursorAdapter}, use
+ * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context,
+ * android.database.Cursor, int)} constructor <em>without</em> passing
+ * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY}
+ * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER}
+ * (that is, use 0 for the flags argument). This prevents the CursorAdapter
+ * from doing its own observing of the Cursor, which is not needed since
+ * when a change happens you will get a new Cursor throw another call
+ * here.
+ * <li> The Loader will release the data once it knows the application
+ * is no longer using it. For example, if the data is
+ * a {@link android.database.Cursor} from a {@link android.content.CursorLoader},
+ * you should not call close() on it yourself. If the Cursor is being placed in a
+ * {@link android.widget.CursorAdapter}, you should use the
+ * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)}
+ * method so that the old Cursor is not closed.
+ * </ul>
+ *
+ * @param loader The Loader that has finished.
+ * @param data The data generated by the Loader.
+ */
+ public void onLoadFinished(Loader<D> loader, D data);
+
+ /**
+ * Called when a previously created loader is being reset, and thus
+ * making its data unavailable. The application should at this point
+ * remove any references it has to the Loader's data.
+ *
+ * @param loader The Loader that is being reset.
+ */
+ public void onLoaderReset(Loader<D> loader);
+ }
+
+ /**
+ * Ensures a loader is initialized and active. If the loader doesn't
+ * already exist, one is created and (if the activity/fragment is currently
+ * started) starts the loader. Otherwise the last created
+ * loader is re-used.
+ *
+ * <p>In either case, the given callback is associated with the loader, and
+ * will be called as the loader state changes. If at the point of call
+ * the caller is in its started state, and the requested loader
+ * already exists and has generated its data, then
+ * callback {@link LoaderCallbacks#onLoadFinished} will
+ * be called immediately (inside of this function), so you must be prepared
+ * for this to happen.
+ *
+ * @param id A unique identifier for this loader. Can be whatever you want.
+ * Identifiers are scoped to a particular LoaderManager instance.
+ * @param args Optional arguments to supply to the loader at construction.
+ * If a loader already exists (a new one does not need to be created), this
+ * parameter will be ignored and the last arguments continue to be used.
+ * @param callback Interface the LoaderManager will call to report about
+ * changes in the state of the loader. Required.
+ */
+ public abstract <D> Loader<D> initLoader(int id, Bundle args,
+ LoaderManager.LoaderCallbacks<D> callback);
+
+ /**
+ * Starts a new or restarts an existing {@link android.content.Loader} in
+ * this manager, registers the callbacks to it,
+ * and (if the activity/fragment is currently started) starts loading it.
+ * If a loader with the same id has previously been
+ * started it will automatically be destroyed when the new loader completes
+ * its work. The callback will be delivered before the old loader
+ * is destroyed.
+ *
+ * @param id A unique identifier for this loader. Can be whatever you want.
+ * Identifiers are scoped to a particular LoaderManager instance.
+ * @param args Optional arguments to supply to the loader at construction.
+ * @param callback Interface the LoaderManager will call to report about
+ * changes in the state of the loader. Required.
+ */
+ public abstract <D> Loader<D> restartLoader(int id, Bundle args,
+ LoaderManager.LoaderCallbacks<D> callback);
+
+ /**
+ * Stops and removes the loader with the given ID. If this loader
+ * had previously reported data to the client through
+ * {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call
+ * will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}.
+ */
+ public abstract void destroyLoader(int id);
+
+ /**
+ * Return the Loader with the given id or null if no matching Loader
+ * is found.
+ */
+ public abstract <D> Loader<D> getLoader(int id);
+
+ /**
+ * Print the LoaderManager's state into the given stream.
+ *
+ * @param prefix Text to print at the front of each line.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer A PrintWriter to which the dump is to be set.
+ * @param args Additional arguments to the dump request.
+ */
+ public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args);
+
+ /**
+ * Control whether the framework's internal loader manager debugging
+ * logs are turned on. If enabled, you will see output in logcat as
+ * the framework performs loader operations.
+ */
+ public static void enableDebugLogging(boolean enabled) {
+ LoaderManagerImpl.DEBUG = enabled;
+ }
+
+ /** @hide for internal testing only */
+ public FragmentHostCallback getFragmentHostCallback() { return null; }
+}
+
+class LoaderManagerImpl extends LoaderManager {
+ static final String TAG = "LoaderManager";
+ static boolean DEBUG = false;
+
+ // These are the currently active loaders. A loader is here
+ // from the time its load is started until it has been explicitly
+ // stopped or restarted by the application.
+ final SparseArray<LoaderInfo> mLoaders = new SparseArray<LoaderInfo>(0);
+
+ // These are previously run loaders. This list is maintained internally
+ // to avoid destroying a loader while an application is still using it.
+ // It allows an application to restart a loader, but continue using its
+ // previously run loader until the new loader's data is available.
+ final SparseArray<LoaderInfo> mInactiveLoaders = new SparseArray<LoaderInfo>(0);
+
+ final String mWho;
+
+ boolean mStarted;
+ boolean mRetaining;
+ boolean mRetainingStarted;
+
+ boolean mCreatingLoader;
+ private FragmentHostCallback mHost;
+
+ final class LoaderInfo implements Loader.OnLoadCompleteListener<Object>,
+ Loader.OnLoadCanceledListener<Object> {
+ final int mId;
+ final Bundle mArgs;
+ LoaderManager.LoaderCallbacks<Object> mCallbacks;
+ Loader<Object> mLoader;
+ boolean mHaveData;
+ boolean mDeliveredData;
+ Object mData;
+ boolean mStarted;
+ boolean mRetaining;
+ boolean mRetainingStarted;
+ boolean mReportNextStart;
+ boolean mDestroyed;
+ boolean mListenerRegistered;
+
+ LoaderInfo mPendingLoader;
+
+ public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks<Object> callbacks) {
+ mId = id;
+ mArgs = args;
+ mCallbacks = callbacks;
+ }
+
+ void start() {
+ if (mRetaining && mRetainingStarted) {
+ // Our owner is started, but we were being retained from a
+ // previous instance in the started state... so there is really
+ // nothing to do here, since the loaders are still started.
+ mStarted = true;
+ return;
+ }
+
+ if (mStarted) {
+ // If loader already started, don't restart.
+ return;
+ }
+
+ mStarted = true;
+
+ if (DEBUG) Log.v(TAG, " Starting: " + this);
+ if (mLoader == null && mCallbacks != null) {
+ mLoader = mCallbacks.onCreateLoader(mId, mArgs);
+ }
+ if (mLoader != null) {
+ if (mLoader.getClass().isMemberClass()
+ && !Modifier.isStatic(mLoader.getClass().getModifiers())) {
+ throw new IllegalArgumentException(
+ "Object returned from onCreateLoader must not be a non-static inner member class: "
+ + mLoader);
+ }
+ if (!mListenerRegistered) {
+ mLoader.registerListener(mId, this);
+ mLoader.registerOnLoadCanceledListener(this);
+ mListenerRegistered = true;
+ }
+ mLoader.startLoading();
+ }
+ }
+
+ void retain() {
+ if (DEBUG) Log.v(TAG, " Retaining: " + this);
+ mRetaining = true;
+ mRetainingStarted = mStarted;
+ mStarted = false;
+ mCallbacks = null;
+ }
+
+ void finishRetain() {
+ if (mRetaining) {
+ if (DEBUG) Log.v(TAG, " Finished Retaining: " + this);
+ mRetaining = false;
+ if (mStarted != mRetainingStarted) {
+ if (!mStarted) {
+ // This loader was retained in a started state, but
+ // at the end of retaining everything our owner is
+ // no longer started... so make it stop.
+ stop();
+ }
+ }
+ }
+
+ if (mStarted && mHaveData && !mReportNextStart) {
+ // This loader has retained its data, either completely across
+ // a configuration change or just whatever the last data set
+ // was after being restarted from a stop, and now at the point of
+ // finishing the retain we find we remain started, have
+ // our data, and the owner has a new callback... so
+ // let's deliver the data now.
+ callOnLoadFinished(mLoader, mData);
+ }
+ }
+
+ void reportStart() {
+ if (mStarted) {
+ if (mReportNextStart) {
+ mReportNextStart = false;
+ if (mHaveData && !mRetaining) {
+ callOnLoadFinished(mLoader, mData);
+ }
+ }
+ }
+ }
+
+ void stop() {
+ if (DEBUG) Log.v(TAG, " Stopping: " + this);
+ mStarted = false;
+ if (!mRetaining) {
+ if (mLoader != null && mListenerRegistered) {
+ // Let the loader know we're done with it
+ mListenerRegistered = false;
+ mLoader.unregisterListener(this);
+ mLoader.unregisterOnLoadCanceledListener(this);
+ mLoader.stopLoading();
+ }
+ }
+ }
+
+ boolean cancel() {
+ if (DEBUG) Log.v(TAG, " Canceling: " + this);
+ if (mStarted && mLoader != null && mListenerRegistered) {
+ final boolean cancelLoadResult = mLoader.cancelLoad();
+ if (!cancelLoadResult) {
+ onLoadCanceled(mLoader);
+ }
+ return cancelLoadResult;
+ }
+ return false;
+ }
+
+ void destroy() {
+ if (DEBUG) Log.v(TAG, " Destroying: " + this);
+ mDestroyed = true;
+ boolean needReset = mDeliveredData;
+ mDeliveredData = false;
+ if (mCallbacks != null && mLoader != null && mHaveData && needReset) {
+ if (DEBUG) Log.v(TAG, " Reseting: " + this);
+ String lastBecause = null;
+ if (mHost != null) {
+ lastBecause = mHost.mFragmentManager.mNoTransactionsBecause;
+ mHost.mFragmentManager.mNoTransactionsBecause = "onLoaderReset";
+ }
+ try {
+ mCallbacks.onLoaderReset(mLoader);
+ } finally {
+ if (mHost != null) {
+ mHost.mFragmentManager.mNoTransactionsBecause = lastBecause;
+ }
+ }
+ }
+ mCallbacks = null;
+ mData = null;
+ mHaveData = false;
+ if (mLoader != null) {
+ if (mListenerRegistered) {
+ mListenerRegistered = false;
+ mLoader.unregisterListener(this);
+ mLoader.unregisterOnLoadCanceledListener(this);
+ }
+ mLoader.reset();
+ }
+ if (mPendingLoader != null) {
+ mPendingLoader.destroy();
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(Loader<Object> loader) {
+ if (DEBUG) Log.v(TAG, "onLoadCanceled: " + this);
+
+ if (mDestroyed) {
+ if (DEBUG) Log.v(TAG, " Ignoring load canceled -- destroyed");
+ return;
+ }
+
+ if (mLoaders.get(mId) != this) {
+ // This cancellation message is not coming from the current active loader.
+ // We don't care about it.
+ if (DEBUG) Log.v(TAG, " Ignoring load canceled -- not active");
+ return;
+ }
+
+ LoaderInfo pending = mPendingLoader;
+ if (pending != null) {
+ // There is a new request pending and we were just
+ // waiting for the old one to cancel or complete before starting
+ // it. So now it is time, switch over to the new loader.
+ if (DEBUG) Log.v(TAG, " Switching to pending loader: " + pending);
+ mPendingLoader = null;
+ mLoaders.put(mId, null);
+ destroy();
+ installLoader(pending);
+ }
+ }
+
+ @Override
+ public void onLoadComplete(Loader<Object> loader, Object data) {
+ if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
+
+ if (mDestroyed) {
+ if (DEBUG) Log.v(TAG, " Ignoring load complete -- destroyed");
+ return;
+ }
+
+ if (mLoaders.get(mId) != this) {
+ // This data is not coming from the current active loader.
+ // We don't care about it.
+ if (DEBUG) Log.v(TAG, " Ignoring load complete -- not active");
+ return;
+ }
+
+ LoaderInfo pending = mPendingLoader;
+ if (pending != null) {
+ // There is a new request pending and we were just
+ // waiting for the old one to complete before starting
+ // it. So now it is time, switch over to the new loader.
+ if (DEBUG) Log.v(TAG, " Switching to pending loader: " + pending);
+ mPendingLoader = null;
+ mLoaders.put(mId, null);
+ destroy();
+ installLoader(pending);
+ return;
+ }
+
+ // Notify of the new data so the app can switch out the old data before
+ // we try to destroy it.
+ if (mData != data || !mHaveData) {
+ mData = data;
+ mHaveData = true;
+ if (mStarted) {
+ callOnLoadFinished(loader, data);
+ }
+ }
+
+ //if (DEBUG) Log.v(TAG, " onLoadFinished returned: " + this);
+
+ // We have now given the application the new loader with its
+ // loaded data, so it should have stopped using the previous
+ // loader. If there is a previous loader on the inactive list,
+ // clean it up.
+ LoaderInfo info = mInactiveLoaders.get(mId);
+ if (info != null && info != this) {
+ info.mDeliveredData = false;
+ info.destroy();
+ mInactiveLoaders.remove(mId);
+ }
+
+ if (mHost != null && !hasRunningLoaders()) {
+ mHost.mFragmentManager.startPendingDeferredFragments();
+ }
+ }
+
+ void callOnLoadFinished(Loader<Object> loader, Object data) {
+ if (mCallbacks != null) {
+ String lastBecause = null;
+ if (mHost != null) {
+ lastBecause = mHost.mFragmentManager.mNoTransactionsBecause;
+ mHost.mFragmentManager.mNoTransactionsBecause = "onLoadFinished";
+ }
+ try {
+ if (DEBUG) Log.v(TAG, " onLoadFinished in " + loader + ": "
+ + loader.dataToString(data));
+ mCallbacks.onLoadFinished(loader, data);
+ } finally {
+ if (mHost != null) {
+ mHost.mFragmentManager.mNoTransactionsBecause = lastBecause;
+ }
+ }
+ mDeliveredData = true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append("LoaderInfo{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" #");
+ sb.append(mId);
+ sb.append(" : ");
+ DebugUtils.buildShortClassTag(mLoader, sb);
+ sb.append("}}");
+ return sb.toString();
+ }
+
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mId="); writer.print(mId);
+ writer.print(" mArgs="); writer.println(mArgs);
+ writer.print(prefix); writer.print("mCallbacks="); writer.println(mCallbacks);
+ writer.print(prefix); writer.print("mLoader="); writer.println(mLoader);
+ if (mLoader != null) {
+ mLoader.dump(prefix + " ", fd, writer, args);
+ }
+ if (mHaveData || mDeliveredData) {
+ writer.print(prefix); writer.print("mHaveData="); writer.print(mHaveData);
+ writer.print(" mDeliveredData="); writer.println(mDeliveredData);
+ writer.print(prefix); writer.print("mData="); writer.println(mData);
+ }
+ writer.print(prefix); writer.print("mStarted="); writer.print(mStarted);
+ writer.print(" mReportNextStart="); writer.print(mReportNextStart);
+ writer.print(" mDestroyed="); writer.println(mDestroyed);
+ writer.print(prefix); writer.print("mRetaining="); writer.print(mRetaining);
+ writer.print(" mRetainingStarted="); writer.print(mRetainingStarted);
+ writer.print(" mListenerRegistered="); writer.println(mListenerRegistered);
+ if (mPendingLoader != null) {
+ writer.print(prefix); writer.println("Pending Loader ");
+ writer.print(mPendingLoader); writer.println(":");
+ mPendingLoader.dump(prefix + " ", fd, writer, args);
+ }
+ }
+ }
+
+ LoaderManagerImpl(String who, FragmentHostCallback host, boolean started) {
+ mWho = who;
+ mHost = host;
+ mStarted = started;
+ }
+
+ void updateHostController(FragmentHostCallback host) {
+ mHost = host;
+ }
+
+ public FragmentHostCallback getFragmentHostCallback() {
+ return mHost;
+ }
+
+ private LoaderInfo createLoader(int id, Bundle args,
+ LoaderManager.LoaderCallbacks<Object> callback) {
+ LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ Loader<Object> loader = callback.onCreateLoader(id, args);
+ info.mLoader = (Loader<Object>)loader;
+ return info;
+ }
+
+ private LoaderInfo createAndInstallLoader(int id, Bundle args,
+ LoaderManager.LoaderCallbacks<Object> callback) {
+ try {
+ mCreatingLoader = true;
+ LoaderInfo info = createLoader(id, args, callback);
+ installLoader(info);
+ return info;
+ } finally {
+ mCreatingLoader = false;
+ }
+ }
+
+ void installLoader(LoaderInfo info) {
+ mLoaders.put(info.mId, info);
+ if (mStarted) {
+ // The activity will start all existing loaders in it's onStart(),
+ // so only start them here if we're past that point of the activitiy's
+ // life cycle
+ info.start();
+ }
+ }
+
+ /**
+ * Call to initialize a particular ID with a Loader. If this ID already
+ * has a Loader associated with it, it is left unchanged and any previous
+ * callbacks replaced with the newly provided ones. If there is not currently
+ * a Loader for the ID, a new one is created and started.
+ *
+ * <p>This function should generally be used when a component is initializing,
+ * to ensure that a Loader it relies on is created. This allows it to re-use
+ * an existing Loader's data if there already is one, so that for example
+ * when an {@link Activity} is re-created after a configuration change it
+ * does not need to re-create its loaders.
+ *
+ * <p>Note that in the case where an existing Loader is re-used, the
+ * <var>args</var> given here <em>will be ignored</em> because you will
+ * continue using the previous Loader.
+ *
+ * @param id A unique (to this LoaderManager instance) identifier under
+ * which to manage the new Loader.
+ * @param args Optional arguments that will be propagated to
+ * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
+ * @param callback Interface implementing management of this Loader. Required.
+ * Its onCreateLoader() method will be called while inside of the function to
+ * instantiate the Loader object.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ if (mCreatingLoader) {
+ throw new IllegalStateException("Called while creating a loader");
+ }
+
+ LoaderInfo info = mLoaders.get(id);
+
+ if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
+
+ if (info == null) {
+ // Loader doesn't already exist; create.
+ info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ if (DEBUG) Log.v(TAG, " Created new loader " + info);
+ } else {
+ if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
+ info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
+ }
+
+ if (info.mHaveData && mStarted) {
+ // If the loader has already generated its data, report it now.
+ info.callOnLoadFinished(info.mLoader, info.mData);
+ }
+
+ return (Loader<D>)info.mLoader;
+ }
+
+ /**
+ * Call to re-create the Loader associated with a particular ID. If there
+ * is currently a Loader associated with this ID, it will be
+ * canceled/stopped/destroyed as appropriate. A new Loader with the given
+ * arguments will be created and its data delivered to you once available.
+ *
+ * <p>This function does some throttling of Loaders. If too many Loaders
+ * have been created for the given ID but not yet generated their data,
+ * new calls to this function will create and return a new Loader but not
+ * actually start it until some previous loaders have completed.
+ *
+ * <p>After calling this function, any previous Loaders associated with
+ * this ID will be considered invalid, and you will receive no further
+ * data updates from them.
+ *
+ * @param id A unique (to this LoaderManager instance) identifier under
+ * which to manage the new Loader.
+ * @param args Optional arguments that will be propagated to
+ * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
+ * @param callback Interface implementing management of this Loader. Required.
+ * Its onCreateLoader() method will be called while inside of the function to
+ * instantiate the Loader object.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ if (mCreatingLoader) {
+ throw new IllegalStateException("Called while creating a loader");
+ }
+
+ LoaderInfo info = mLoaders.get(id);
+ if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
+ if (info != null) {
+ LoaderInfo inactive = mInactiveLoaders.get(id);
+ if (inactive != null) {
+ if (info.mHaveData) {
+ // This loader now has data... we are probably being
+ // called from within onLoadComplete, where we haven't
+ // yet destroyed the last inactive loader. So just do
+ // that now.
+ if (DEBUG) Log.v(TAG, " Removing last inactive loader: " + info);
+ inactive.mDeliveredData = false;
+ inactive.destroy();
+ info.mLoader.abandon();
+ mInactiveLoaders.put(id, info);
+ } else {
+ // We already have an inactive loader for this ID that we are
+ // waiting for! Try to cancel; if this returns true then the task is still
+ // running and we have more work to do.
+ if (!info.cancel()) {
+ // The current Loader has not been started or was successfully canceled,
+ // we thus have no reason to keep it around. Remove it and a new
+ // LoaderInfo will be created below.
+ if (DEBUG) Log.v(TAG, " Current loader is stopped; replacing");
+ mLoaders.put(id, null);
+ info.destroy();
+ } else {
+ // Now we have three active loaders... we'll queue
+ // up this request to be processed once one of the other loaders
+ // finishes.
+ if (DEBUG) Log.v(TAG,
+ " Current loader is running; configuring pending loader");
+ if (info.mPendingLoader != null) {
+ if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader);
+ info.mPendingLoader.destroy();
+ info.mPendingLoader = null;
+ }
+ if (DEBUG) Log.v(TAG, " Enqueuing as new pending loader");
+ info.mPendingLoader = createLoader(id, args,
+ (LoaderManager.LoaderCallbacks<Object>)callback);
+ return (Loader<D>)info.mPendingLoader.mLoader;
+ }
+ }
+ } else {
+ // Keep track of the previous instance of this loader so we can destroy
+ // it when the new one completes.
+ if (DEBUG) Log.v(TAG, " Making last loader inactive: " + info);
+ info.mLoader.abandon();
+ mInactiveLoaders.put(id, info);
+ }
+ }
+
+ info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ return (Loader<D>)info.mLoader;
+ }
+
+ /**
+ * Rip down, tear apart, shred to pieces a current Loader ID. After returning
+ * from this function, any Loader objects associated with this ID are
+ * destroyed. Any data associated with them is destroyed. You better not
+ * be using it when you do this.
+ * @param id Identifier of the Loader to be destroyed.
+ */
+ public void destroyLoader(int id) {
+ if (mCreatingLoader) {
+ throw new IllegalStateException("Called while creating a loader");
+ }
+
+ if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
+ int idx = mLoaders.indexOfKey(id);
+ if (idx >= 0) {
+ LoaderInfo info = mLoaders.valueAt(idx);
+ mLoaders.removeAt(idx);
+ info.destroy();
+ }
+ idx = mInactiveLoaders.indexOfKey(id);
+ if (idx >= 0) {
+ LoaderInfo info = mInactiveLoaders.valueAt(idx);
+ mInactiveLoaders.removeAt(idx);
+ info.destroy();
+ }
+ if (mHost != null && !hasRunningLoaders()) {
+ mHost.mFragmentManager.startPendingDeferredFragments();
+ }
+ }
+
+ /**
+ * Return the most recent Loader object associated with the
+ * given ID.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> getLoader(int id) {
+ if (mCreatingLoader) {
+ throw new IllegalStateException("Called while creating a loader");
+ }
+
+ LoaderInfo loaderInfo = mLoaders.get(id);
+ if (loaderInfo != null) {
+ if (loaderInfo.mPendingLoader != null) {
+ return (Loader<D>)loaderInfo.mPendingLoader.mLoader;
+ }
+ return (Loader<D>)loaderInfo.mLoader;
+ }
+ return null;
+ }
+
+ void doStart() {
+ if (DEBUG) Log.v(TAG, "Starting in " + this);
+ if (mStarted) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "Called doStart when already started: " + this, e);
+ return;
+ }
+
+ mStarted = true;
+
+ // Call out to sub classes so they can start their loaders
+ // Let the existing loaders know that we want to be notified when a load is complete
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).start();
+ }
+ }
+
+ void doStop() {
+ if (DEBUG) Log.v(TAG, "Stopping in " + this);
+ if (!mStarted) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "Called doStop when not started: " + this, e);
+ return;
+ }
+
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).stop();
+ }
+ mStarted = false;
+ }
+
+ void doRetain() {
+ if (DEBUG) Log.v(TAG, "Retaining in " + this);
+ if (!mStarted) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "Called doRetain when not started: " + this, e);
+ return;
+ }
+
+ mRetaining = true;
+ mStarted = false;
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).retain();
+ }
+ }
+
+ void finishRetain() {
+ if (mRetaining) {
+ if (DEBUG) Log.v(TAG, "Finished Retaining in " + this);
+
+ mRetaining = false;
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).finishRetain();
+ }
+ }
+ }
+
+ void doReportNextStart() {
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).mReportNextStart = true;
+ }
+ }
+
+ void doReportStart() {
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).reportStart();
+ }
+ }
+
+ void doDestroy() {
+ if (!mRetaining) {
+ if (DEBUG) Log.v(TAG, "Destroying Active in " + this);
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).destroy();
+ }
+ mLoaders.clear();
+ }
+
+ if (DEBUG) Log.v(TAG, "Destroying Inactive in " + this);
+ for (int i = mInactiveLoaders.size()-1; i >= 0; i--) {
+ mInactiveLoaders.valueAt(i).destroy();
+ }
+ mInactiveLoaders.clear();
+ mHost = null;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("LoaderManager{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" in ");
+ DebugUtils.buildShortClassTag(mHost, sb);
+ sb.append("}}");
+ return sb.toString();
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (mLoaders.size() > 0) {
+ writer.print(prefix); writer.println("Active Loaders:");
+ String innerPrefix = prefix + " ";
+ for (int i=0; i < mLoaders.size(); i++) {
+ LoaderInfo li = mLoaders.valueAt(i);
+ writer.print(prefix); writer.print(" #"); writer.print(mLoaders.keyAt(i));
+ writer.print(": "); writer.println(li.toString());
+ li.dump(innerPrefix, fd, writer, args);
+ }
+ }
+ if (mInactiveLoaders.size() > 0) {
+ writer.print(prefix); writer.println("Inactive Loaders:");
+ String innerPrefix = prefix + " ";
+ for (int i=0; i < mInactiveLoaders.size(); i++) {
+ LoaderInfo li = mInactiveLoaders.valueAt(i);
+ writer.print(prefix); writer.print(" #"); writer.print(mInactiveLoaders.keyAt(i));
+ writer.print(": "); writer.println(li.toString());
+ li.dump(innerPrefix, fd, writer, args);
+ }
+ }
+ }
+
+ public boolean hasRunningLoaders() {
+ boolean loadersRunning = false;
+ final int count = mLoaders.size();
+ for (int i = 0; i < count; i++) {
+ final LoaderInfo li = mLoaders.valueAt(i);
+ loadersRunning |= li.mStarted && !li.mDeliveredData;
+ }
+ return loadersRunning;
+ }
+}
diff --git a/android/app/LocalActivityManager.java b/android/app/LocalActivityManager.java
new file mode 100644
index 00000000..3b273bc1
--- /dev/null
+++ b/android/app/LocalActivityManager.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Window;
+import com.android.internal.content.ReferrerIntent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>Helper class for managing multiple running embedded activities in the same
+ * process. This class is not normally used directly, but rather created for
+ * you as part of the {@link android.app.ActivityGroup} implementation.
+ *
+ * @see ActivityGroup
+ *
+ * @deprecated Use the new {@link Fragment} and {@link FragmentManager} APIs
+ * instead; these are also
+ * available on older platforms through the Android compatibility package.
+ */
+@Deprecated
+public class LocalActivityManager {
+ private static final String TAG = "LocalActivityManager";
+ private static final boolean localLOGV = false;
+
+ // Internal token for an Activity being managed by LocalActivityManager.
+ private static class LocalActivityRecord extends Binder {
+ LocalActivityRecord(String _id, Intent _intent) {
+ id = _id;
+ intent = _intent;
+ }
+
+ final String id; // Unique name of this record.
+ Intent intent; // Which activity to run here.
+ ActivityInfo activityInfo; // Package manager info about activity.
+ Activity activity; // Currently instantiated activity.
+ Window window; // Activity's top-level window.
+ Bundle instanceState; // Last retrieved freeze state.
+ int curState = RESTORED; // Current state the activity is in.
+ }
+
+ static final int RESTORED = 0; // State restored, but no startActivity().
+ static final int INITIALIZING = 1; // Ready to launch (after startActivity()).
+ static final int CREATED = 2; // Created, not started or resumed.
+ static final int STARTED = 3; // Created and started, not resumed.
+ static final int RESUMED = 4; // Created started and resumed.
+ static final int DESTROYED = 5; // No longer with us.
+
+ /** Thread our activities are running in. */
+ private final ActivityThread mActivityThread;
+ /** The containing activity that owns the activities we create. */
+ private final Activity mParent;
+
+ /** The activity that is currently resumed. */
+ private LocalActivityRecord mResumed;
+ /** id -> record of all known activities. */
+ private final Map<String, LocalActivityRecord> mActivities
+ = new HashMap<String, LocalActivityRecord>();
+ /** array of all known activities for easy iterating. */
+ private final ArrayList<LocalActivityRecord> mActivityArray
+ = new ArrayList<LocalActivityRecord>();
+
+ /** True if only one activity can be resumed at a time */
+ private boolean mSingleMode;
+
+ /** Set to true once we find out the container is finishing. */
+ private boolean mFinishing;
+
+ /** Current state the owner (ActivityGroup) is in */
+ private int mCurState = INITIALIZING;
+
+ /** String ids of running activities starting with least recently used. */
+ // TODO: put back in stopping of activities.
+ //private List<LocalActivityRecord> mLRU = new ArrayList();
+
+ /**
+ * Create a new LocalActivityManager for holding activities running within
+ * the given <var>parent</var>.
+ *
+ * @param parent the host of the embedded activities
+ * @param singleMode True if the LocalActivityManger should keep a maximum
+ * of one activity resumed
+ */
+ public LocalActivityManager(Activity parent, boolean singleMode) {
+ mActivityThread = ActivityThread.currentActivityThread();
+ mParent = parent;
+ mSingleMode = singleMode;
+ }
+
+ private void moveToState(LocalActivityRecord r, int desiredState) {
+ if (r.curState == RESTORED || r.curState == DESTROYED) {
+ // startActivity() has not yet been called, so nothing to do.
+ return;
+ }
+
+ if (r.curState == INITIALIZING) {
+ // Get the lastNonConfigurationInstance for the activity
+ HashMap<String, Object> lastNonConfigurationInstances =
+ mParent.getLastNonConfigurationChildInstances();
+ Object instanceObj = null;
+ if (lastNonConfigurationInstances != null) {
+ instanceObj = lastNonConfigurationInstances.get(r.id);
+ }
+ Activity.NonConfigurationInstances instance = null;
+ if (instanceObj != null) {
+ instance = new Activity.NonConfigurationInstances();
+ instance.activity = instanceObj;
+ }
+
+ // We need to have always created the activity.
+ if (localLOGV) Log.v(TAG, r.id + ": starting " + r.intent);
+ if (r.activityInfo == null) {
+ r.activityInfo = mActivityThread.resolveActivityInfo(r.intent);
+ }
+ r.activity = mActivityThread.startActivityNow(
+ mParent, r.id, r.intent, r.activityInfo, r, r.instanceState, instance);
+ if (r.activity == null) {
+ return;
+ }
+ r.window = r.activity.getWindow();
+ r.instanceState = null;
+ r.curState = STARTED;
+
+ if (desiredState == RESUMED) {
+ if (localLOGV) Log.v(TAG, r.id + ": resuming");
+ mActivityThread.performResumeActivity(r, true, "moveToState-INITIALIZING");
+ r.curState = RESUMED;
+ }
+
+ // Don't do anything more here. There is an important case:
+ // if this is being done as part of onCreate() of the group, then
+ // the launching of the activity gets its state a little ahead
+ // of our own (it is now STARTED, while we are only CREATED).
+ // If we just leave things as-is, we'll deal with it as the
+ // group's state catches up.
+ return;
+ }
+
+ switch (r.curState) {
+ case CREATED:
+ if (desiredState == STARTED) {
+ if (localLOGV) Log.v(TAG, r.id + ": restarting");
+ mActivityThread.performRestartActivity(r);
+ r.curState = STARTED;
+ }
+ if (desiredState == RESUMED) {
+ if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming");
+ mActivityThread.performRestartActivity(r);
+ mActivityThread.performResumeActivity(r, true, "moveToState-CREATED");
+ r.curState = RESUMED;
+ }
+ return;
+
+ case STARTED:
+ if (desiredState == RESUMED) {
+ // Need to resume it...
+ if (localLOGV) Log.v(TAG, r.id + ": resuming");
+ mActivityThread.performResumeActivity(r, true, "moveToState-STARTED");
+ r.instanceState = null;
+ r.curState = RESUMED;
+ }
+ if (desiredState == CREATED) {
+ if (localLOGV) Log.v(TAG, r.id + ": stopping");
+ mActivityThread.performStopActivity(r, false, "moveToState-STARTED");
+ r.curState = CREATED;
+ }
+ return;
+
+ case RESUMED:
+ if (desiredState == STARTED) {
+ if (localLOGV) Log.v(TAG, r.id + ": pausing");
+ performPause(r, mFinishing);
+ r.curState = STARTED;
+ }
+ if (desiredState == CREATED) {
+ if (localLOGV) Log.v(TAG, r.id + ": pausing");
+ performPause(r, mFinishing);
+ if (localLOGV) Log.v(TAG, r.id + ": stopping");
+ mActivityThread.performStopActivity(r, false, "moveToState-RESUMED");
+ r.curState = CREATED;
+ }
+ return;
+ }
+ }
+
+ private void performPause(LocalActivityRecord r, boolean finishing) {
+ final boolean needState = r.instanceState == null;
+ final Bundle instanceState = mActivityThread.performPauseActivity(
+ r, finishing, needState, "performPause");
+ if (needState) {
+ r.instanceState = instanceState;
+ }
+ }
+
+ /**
+ * Start a new activity running in the group. Every activity you start
+ * must have a unique string ID associated with it -- this is used to keep
+ * track of the activity, so that if you later call startActivity() again
+ * on it the same activity object will be retained.
+ *
+ * <p>When there had previously been an activity started under this id,
+ * it may either be destroyed and a new one started, or the current
+ * one re-used, based on these conditions, in order:</p>
+ *
+ * <ul>
+ * <li> If the Intent maps to a different activity component than is
+ * currently running, the current activity is finished and a new one
+ * started.
+ * <li> If the current activity uses a non-multiple launch mode (such
+ * as singleTop), or the Intent has the
+ * {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} flag set, then the current
+ * activity will remain running and its
+ * {@link Activity#onNewIntent(Intent) Activity.onNewIntent()} method
+ * called.
+ * <li> If the new Intent is the same (excluding extras) as the previous
+ * one, and the new Intent does not have the
+ * {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set, then the current activity
+ * will remain running as-is.
+ * <li> Otherwise, the current activity will be finished and a new
+ * one started.
+ * </ul>
+ *
+ * <p>If the given Intent can not be resolved to an available Activity,
+ * this method throws {@link android.content.ActivityNotFoundException}.
+ *
+ * <p>Warning: There is an issue where, if the Intent does not
+ * include an explicit component, we can restore the state for a different
+ * activity class than was previously running when the state was saved (if
+ * the set of available activities changes between those points).
+ *
+ * @param id Unique identifier of the activity to be started
+ * @param intent The Intent describing the activity to be started
+ *
+ * @return Returns the window of the activity. The caller needs to take
+ * care of adding this window to a view hierarchy, and likewise dealing
+ * with removing the old window if the activity has changed.
+ *
+ * @throws android.content.ActivityNotFoundException
+ */
+ public Window startActivity(String id, Intent intent) {
+ if (mCurState == INITIALIZING) {
+ throw new IllegalStateException(
+ "Activities can't be added until the containing group has been created.");
+ }
+
+ boolean adding = false;
+ boolean sameIntent = false;
+
+ ActivityInfo aInfo = null;
+
+ // Already have information about the new activity id?
+ LocalActivityRecord r = mActivities.get(id);
+ if (r == null) {
+ // Need to create it...
+ r = new LocalActivityRecord(id, intent);
+ adding = true;
+ } else if (r.intent != null) {
+ sameIntent = r.intent.filterEquals(intent);
+ if (sameIntent) {
+ // We are starting the same activity.
+ aInfo = r.activityInfo;
+ }
+ }
+ if (aInfo == null) {
+ aInfo = mActivityThread.resolveActivityInfo(intent);
+ }
+
+ // Pause the currently running activity if there is one and only a single
+ // activity is allowed to be running at a time.
+ if (mSingleMode) {
+ LocalActivityRecord old = mResumed;
+
+ // If there was a previous activity, and it is not the current
+ // activity, we need to stop it.
+ if (old != null && old != r && mCurState == RESUMED) {
+ moveToState(old, STARTED);
+ }
+ }
+
+ if (adding) {
+ // It's a brand new world.
+ mActivities.put(id, r);
+ mActivityArray.add(r);
+ } else if (r.activityInfo != null) {
+ // If the new activity is the same as the current one, then
+ // we may be able to reuse it.
+ if (aInfo == r.activityInfo ||
+ (aInfo.name.equals(r.activityInfo.name) &&
+ aInfo.packageName.equals(r.activityInfo.packageName))) {
+ if (aInfo.launchMode != ActivityInfo.LAUNCH_MULTIPLE ||
+ (intent.getFlags()&Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0) {
+ // The activity wants onNewIntent() called.
+ ArrayList<ReferrerIntent> intents = new ArrayList<>(1);
+ intents.add(new ReferrerIntent(intent, mParent.getPackageName()));
+ if (localLOGV) Log.v(TAG, r.id + ": new intent");
+ mActivityThread.performNewIntents(r, intents, false /* andPause */);
+ r.intent = intent;
+ moveToState(r, mCurState);
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+ if (sameIntent &&
+ (intent.getFlags()&Intent.FLAG_ACTIVITY_CLEAR_TOP) == 0) {
+ // We are showing the same thing, so this activity is
+ // just resumed and stays as-is.
+ r.intent = intent;
+ moveToState(r, mCurState);
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+ }
+
+ // The new activity is different than the current one, or it
+ // is a multiple launch activity, so we need to destroy what
+ // is currently there.
+ performDestroy(r, true);
+ }
+
+ r.intent = intent;
+ r.curState = INITIALIZING;
+ r.activityInfo = aInfo;
+
+ moveToState(r, mCurState);
+
+ // When in single mode keep track of the current activity
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+
+ private Window performDestroy(LocalActivityRecord r, boolean finish) {
+ Window win;
+ win = r.window;
+ if (r.curState == RESUMED && !finish) {
+ performPause(r, finish);
+ }
+ if (localLOGV) Log.v(TAG, r.id + ": destroying");
+ mActivityThread.performDestroyActivity(r, finish);
+ r.activity = null;
+ r.window = null;
+ if (finish) {
+ r.instanceState = null;
+ }
+ r.curState = DESTROYED;
+ return win;
+ }
+
+ /**
+ * Destroy the activity associated with a particular id. This activity
+ * will go through the normal lifecycle events and fine onDestroy(), and
+ * then the id removed from the group.
+ *
+ * @param id Unique identifier of the activity to be destroyed
+ * @param finish If true, this activity will be finished, so its id and
+ * all state are removed from the group.
+ *
+ * @return Returns the window that was used to display the activity, or
+ * null if there was none.
+ */
+ public Window destroyActivity(String id, boolean finish) {
+ LocalActivityRecord r = mActivities.get(id);
+ Window win = null;
+ if (r != null) {
+ win = performDestroy(r, finish);
+ if (finish) {
+ mActivities.remove(id);
+ mActivityArray.remove(r);
+ }
+ }
+ return win;
+ }
+
+ /**
+ * Retrieve the Activity that is currently running.
+ *
+ * @return the currently running (resumed) Activity, or null if there is
+ * not one
+ *
+ * @see #startActivity
+ * @see #getCurrentId
+ */
+ public Activity getCurrentActivity() {
+ return mResumed != null ? mResumed.activity : null;
+ }
+
+ /**
+ * Retrieve the ID of the activity that is currently running.
+ *
+ * @return the ID of the currently running (resumed) Activity, or null if
+ * there is not one
+ *
+ * @see #startActivity
+ * @see #getCurrentActivity
+ */
+ public String getCurrentId() {
+ return mResumed != null ? mResumed.id : null;
+ }
+
+ /**
+ * Return the Activity object associated with a string ID.
+ *
+ * @see #startActivity
+ *
+ * @return the associated Activity object, or null if the id is unknown or
+ * its activity is not currently instantiated
+ */
+ public Activity getActivity(String id) {
+ LocalActivityRecord r = mActivities.get(id);
+ return r != null ? r.activity : null;
+ }
+
+ /**
+ * Restore a state that was previously returned by {@link #saveInstanceState}. This
+ * adds to the activity group information about all activity IDs that had
+ * previously been saved, even if they have not been started yet, so if the
+ * user later navigates to them the correct state will be restored.
+ *
+ * <p>Note: This does <b>not</b> change the current running activity, or
+ * start whatever activity was previously running when the state was saved.
+ * That is up to the client to do, in whatever way it thinks is best.
+ *
+ * @param state a previously saved state; does nothing if this is null
+ *
+ * @see #saveInstanceState
+ */
+ public void dispatchCreate(Bundle state) {
+ if (state != null) {
+ for (String id : state.keySet()) {
+ try {
+ final Bundle astate = state.getBundle(id);
+ LocalActivityRecord r = mActivities.get(id);
+ if (r != null) {
+ r.instanceState = astate;
+ } else {
+ r = new LocalActivityRecord(id, null);
+ r.instanceState = astate;
+ mActivities.put(id, r);
+ mActivityArray.add(r);
+ }
+ } catch (Exception e) {
+ // Recover from -all- app errors.
+ Log.e(TAG, "Exception thrown when restoring LocalActivityManager state", e);
+ }
+ }
+ }
+
+ mCurState = CREATED;
+ }
+
+ /**
+ * Retrieve the state of all activities known by the group. For
+ * activities that have previously run and are now stopped or finished, the
+ * last saved state is used. For the current running activity, its
+ * {@link Activity#onSaveInstanceState} is called to retrieve its current state.
+ *
+ * @return a Bundle holding the newly created state of all known activities
+ *
+ * @see #dispatchCreate
+ */
+ public Bundle saveInstanceState() {
+ Bundle state = null;
+
+ // FIXME: child activities will freeze as part of onPaused. Do we
+ // need to do this here?
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ final LocalActivityRecord r = mActivityArray.get(i);
+ if (state == null) {
+ state = new Bundle();
+ }
+ if ((r.instanceState != null || r.curState == RESUMED)
+ && r.activity != null) {
+ // We need to save the state now, if we don't currently
+ // already have it or the activity is currently resumed.
+ final Bundle childState = new Bundle();
+ r.activity.performSaveInstanceState(childState);
+ r.instanceState = childState;
+ }
+ if (r.instanceState != null) {
+ state.putBundle(r.id, r.instanceState);
+ }
+ }
+
+ return state;
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onResume} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onResume
+ */
+ public void dispatchResume() {
+ mCurState = RESUMED;
+ if (mSingleMode) {
+ if (mResumed != null) {
+ moveToState(mResumed, RESUMED);
+ }
+ } else {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ moveToState(mActivityArray.get(i), RESUMED);
+ }
+ }
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onPause} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @param finishing set to true if the parent activity has been finished;
+ * this can be determined by calling
+ * Activity.isFinishing()
+ *
+ * @see Activity#onPause
+ * @see Activity#isFinishing
+ */
+ public void dispatchPause(boolean finishing) {
+ if (finishing) {
+ mFinishing = true;
+ }
+ mCurState = STARTED;
+ if (mSingleMode) {
+ if (mResumed != null) {
+ moveToState(mResumed, STARTED);
+ }
+ } else {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ if (r.curState == RESUMED) {
+ moveToState(r, STARTED);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onStop} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onStop
+ */
+ public void dispatchStop() {
+ mCurState = CREATED;
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ moveToState(r, CREATED);
+ }
+ }
+
+ /**
+ * Call onRetainNonConfigurationInstance on each child activity and store the
+ * results in a HashMap by id. Only construct the HashMap if there is a non-null
+ * object to store. Note that this does not support nested ActivityGroups.
+ *
+ * {@hide}
+ */
+ public HashMap<String,Object> dispatchRetainNonConfigurationInstance() {
+ HashMap<String,Object> instanceMap = null;
+
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ if ((r != null) && (r.activity != null)) {
+ Object instance = r.activity.onRetainNonConfigurationInstance();
+ if (instance != null) {
+ if (instanceMap == null) {
+ instanceMap = new HashMap<String,Object>();
+ }
+ instanceMap.put(r.id, instance);
+ }
+ }
+ }
+ return instanceMap;
+ }
+
+ /**
+ * Remove all activities from this LocalActivityManager, performing an
+ * {@link Activity#onDestroy} on any that are currently instantiated.
+ */
+ public void removeAllActivities() {
+ dispatchDestroy(true);
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onDestroy} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onDestroy
+ */
+ public void dispatchDestroy(boolean finishing) {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ if (localLOGV) Log.v(TAG, r.id + ": destroying");
+ mActivityThread.performDestroyActivity(r, finishing);
+ }
+ mActivities.clear();
+ mActivityArray.clear();
+ }
+}
diff --git a/android/app/MediaRouteActionProvider.java b/android/app/MediaRouteActionProvider.java
new file mode 100644
index 00000000..85ca0123
--- /dev/null
+++ b/android/app/MediaRouteActionProvider.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteInfo;
+import android.util.Log;
+import android.view.ActionProvider;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * The media route action provider displays a {@link MediaRouteButton media route button}
+ * in the application's {@link ActionBar} to allow the user to select routes and
+ * to control the currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying the route types with the {@link #setRouteTypes} method.
+ * </p><p>
+ * Refer to {@link MediaRouteButton} for a description of the button that will
+ * appear in the action bar menu. Note that instead of disabling the button
+ * when no routes are available, the action provider will instead make the
+ * menu item invisible. In this way, the button will only be visible when it
+ * is possible for the user to discover and select a matching route.
+ * </p>
+ */
+public class MediaRouteActionProvider extends ActionProvider {
+ private static final String TAG = "MediaRouteActionProvider";
+
+ private final Context mContext;
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private int mRouteTypes;
+ private MediaRouteButton mButton;
+ private View.OnClickListener mExtendedSettingsListener;
+
+ public MediaRouteActionProvider(Context context) {
+ super(context);
+
+ mContext = context;
+ mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ mCallback = new MediaRouterCallback(this);
+
+ // Start with live audio by default.
+ // TODO Update this when new route types are added; segment by API level
+ // when different route types were added.
+ setRouteTypes(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
+ }
+
+ /**
+ * Sets the types of routes that will be shown in the media route chooser dialog
+ * launched by this button.
+ *
+ * @param types The route types to match.
+ */
+ public void setRouteTypes(int types) {
+ if (mRouteTypes != types) {
+ // FIXME: We currently have no way of knowing whether the action provider
+ // is still needed by the UI. Unfortunately this means the action provider
+ // may leak callbacks until garbage collection occurs. This may result in
+ // media route providers doing more work than necessary in the short term
+ // while trying to discover routes that are no longer of interest to the
+ // application. To solve this problem, the action provider will need some
+ // indication from the framework that it is being destroyed.
+ if (mRouteTypes != 0) {
+ mRouter.removeCallback(mCallback);
+ }
+ mRouteTypes = types;
+ if (types != 0) {
+ mRouter.addCallback(types, mCallback,
+ MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
+ }
+ refreshRoute();
+
+ if (mButton != null) {
+ mButton.setRouteTypes(mRouteTypes);
+ }
+ }
+ }
+
+ public void setExtendedSettingsClickListener(View.OnClickListener listener) {
+ mExtendedSettingsListener = listener;
+ if (mButton != null) {
+ mButton.setExtendedSettingsClickListener(listener);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public View onCreateActionView() {
+ throw new UnsupportedOperationException("Use onCreateActionView(MenuItem) instead.");
+ }
+
+ @Override
+ public View onCreateActionView(MenuItem item) {
+ if (mButton != null) {
+ Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
+ "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
+ "Abandoning the old one...");
+ }
+
+ mButton = new MediaRouteButton(mContext);
+ mButton.setRouteTypes(mRouteTypes);
+ mButton.setExtendedSettingsClickListener(mExtendedSettingsListener);
+ mButton.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ return mButton;
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ if (mButton != null) {
+ return mButton.showDialogInternal();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean overridesItemVisibility() {
+ return true;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return mRouter.isRouteAvailable(mRouteTypes,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+
+ private void refreshRoute() {
+ refreshVisibility();
+ }
+
+ private static class MediaRouterCallback extends MediaRouter.SimpleCallback {
+ private final WeakReference<MediaRouteActionProvider> mProviderWeak;
+
+ public MediaRouterCallback(MediaRouteActionProvider provider) {
+ mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ private void refreshRoute(MediaRouter router) {
+ MediaRouteActionProvider provider = mProviderWeak.get();
+ if (provider != null) {
+ provider.refreshRoute();
+ } else {
+ router.removeCallback(this);
+ }
+ }
+ }
+}
diff --git a/android/app/MediaRouteButton.java b/android/app/MediaRouteButton.java
new file mode 100644
index 00000000..a4a89fa3
--- /dev/null
+++ b/android/app/MediaRouteButton.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.internal.R;
+import com.android.internal.app.MediaRouteDialogPresenter;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaRouter;
+import android.media.MediaRouter.RouteGroup;
+import android.media.MediaRouter.RouteInfo;
+import android.util.AttributeSet;
+import android.view.SoundEffectConstants;
+import android.view.View;
+
+public class MediaRouteButton extends View {
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private int mRouteTypes;
+
+ private boolean mAttachedToWindow;
+
+ private Drawable mRemoteIndicator;
+ private boolean mRemoteActive;
+ private boolean mIsConnecting;
+
+ private int mMinWidth;
+ private int mMinHeight;
+
+ private OnClickListener mExtendedSettingsClickListener;
+
+ // The checked state is used when connected to a remote route.
+ private static final int[] CHECKED_STATE_SET = {
+ R.attr.state_checked
+ };
+
+ // The activated state is used while connecting to a remote route.
+ private static final int[] ACTIVATED_STATE_SET = {
+ R.attr.state_activated
+ };
+
+ public MediaRouteButton(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MediaRouteButton(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ mCallback = new MediaRouterCallback();
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes);
+ setRemoteIndicatorDrawable(a.getDrawable(
+ com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
+ mMinWidth = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.MediaRouteButton_minWidth, 0);
+ mMinHeight = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.MediaRouteButton_minHeight, 0);
+ final int routeTypes = a.getInteger(
+ com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
+ MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
+ a.recycle();
+
+ setClickable(true);
+
+ setRouteTypes(routeTypes);
+ }
+
+ /**
+ * Gets the media route types for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @return The route types.
+ */
+ public int getRouteTypes() {
+ return mRouteTypes;
+ }
+
+ /**
+ * Sets the types of routes that will be shown in the media route chooser dialog
+ * launched by this button.
+ *
+ * @param types The route types to match.
+ */
+ public void setRouteTypes(int types) {
+ if (mRouteTypes != types) {
+ if (mAttachedToWindow && mRouteTypes != 0) {
+ mRouter.removeCallback(mCallback);
+ }
+
+ mRouteTypes = types;
+
+ if (mAttachedToWindow && types != 0) {
+ mRouter.addCallback(types, mCallback,
+ MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
+ }
+
+ refreshRoute();
+ }
+ }
+
+ public void setExtendedSettingsClickListener(OnClickListener listener) {
+ mExtendedSettingsClickListener = listener;
+ }
+
+ /**
+ * Show the route chooser or controller dialog.
+ * <p>
+ * If the default route is selected or if the currently selected route does
+ * not match the {@link #getRouteTypes route types}, then shows the route chooser dialog.
+ * Otherwise, shows the route controller dialog to offer the user
+ * a choice to disconnect from the route or perform other control actions
+ * such as setting the route's volume.
+ * </p><p>
+ * This will attach a {@link DialogFragment} to the containing Activity.
+ * </p>
+ */
+ public void showDialog() {
+ showDialogInternal();
+ }
+
+ boolean showDialogInternal() {
+ if (!mAttachedToWindow) {
+ return false;
+ }
+
+ DialogFragment f = MediaRouteDialogPresenter.showDialogFragment(getActivity(),
+ mRouteTypes, mExtendedSettingsClickListener);
+ return f != null;
+ }
+
+ private Activity getActivity() {
+ // Gross way of unwrapping the Activity so we can get the FragmentManager
+ Context context = getContext();
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ return (Activity)context;
+ }
+ context = ((ContextWrapper)context).getBaseContext();
+ }
+ throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
+ }
+
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ super.setContentDescription(contentDescription);
+ setTooltipText(contentDescription);
+ }
+
+ @Override
+ public boolean performClick() {
+ // Send the appropriate accessibility events and call listeners
+ boolean handled = super.performClick();
+ if (!handled) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return showDialogInternal() || handled;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ // Technically we should be handling this more completely, but these
+ // are implementation details here. Checked is used to express the connecting
+ // drawable state and it's mutually exclusive with activated for the purposes
+ // of state selection here.
+ if (mIsConnecting) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ } else if (mRemoteActive) {
+ mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final Drawable remoteIndicator = mRemoteIndicator;
+ if (remoteIndicator != null && remoteIndicator.isStateful()
+ && remoteIndicator.setState(getDrawableState())) {
+ invalidateDrawable(remoteIndicator);
+ }
+ }
+
+ private void setRemoteIndicatorDrawable(Drawable d) {
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setCallback(null);
+ unscheduleDrawable(mRemoteIndicator);
+ }
+ mRemoteIndicator = d;
+ if (d != null) {
+ d.setCallback(this);
+ d.setState(getDrawableState());
+ d.setVisible(getVisibility() == VISIBLE, false);
+ }
+
+ refreshDrawableState();
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || who == mRemoteIndicator;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ if (mRouteTypes != 0) {
+ mRouter.addCallback(mRouteTypes, mCallback,
+ MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
+ }
+ refreshRoute();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ if (mRouteTypes != 0) {
+ mRouter.removeCallback(mCallback);
+ }
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ final int width = Math.max(mMinWidth, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0);
+ final int height = Math.max(mMinHeight, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0);
+
+ int measuredWidth;
+ switch (widthMode) {
+ case MeasureSpec.EXACTLY:
+ measuredWidth = widthSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredWidth = Math.min(widthSize, width);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredWidth = width;
+ break;
+ }
+
+ int measuredHeight;
+ switch (heightMode) {
+ case MeasureSpec.EXACTLY:
+ measuredHeight = heightSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredHeight = Math.min(heightSize, height);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredHeight = height;
+ break;
+ }
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mRemoteIndicator == null) return;
+
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+ final int top = getPaddingTop();
+ final int bottom = getHeight() - getPaddingBottom();
+
+ final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
+ final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
+ final int drawLeft = left + (right - left - drawWidth) / 2;
+ final int drawTop = top + (bottom - top - drawHeight) / 2;
+
+ mRemoteIndicator.setBounds(drawLeft, drawTop,
+ drawLeft + drawWidth, drawTop + drawHeight);
+ mRemoteIndicator.draw(canvas);
+ }
+
+ private void refreshRoute() {
+ final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
+ final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes);
+ final boolean isConnecting = isRemote && route.isConnecting();
+ boolean needsRefresh = false;
+ if (mRemoteActive != isRemote) {
+ mRemoteActive = isRemote;
+ needsRefresh = true;
+ }
+ if (mIsConnecting != isConnecting) {
+ mIsConnecting = isConnecting;
+ needsRefresh = true;
+ }
+
+ if (needsRefresh) {
+ refreshDrawableState();
+ }
+ if (mAttachedToWindow) {
+ setEnabled(mRouter.isRouteAvailable(mRouteTypes,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
+ }
+ if (mRemoteIndicator != null
+ && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
+ AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent();
+ if (mAttachedToWindow) {
+ if ((needsRefresh || isConnecting) && !curDrawable.isRunning()) {
+ curDrawable.start();
+ }
+ } else if (isRemote && !isConnecting) {
+ // When the route is already connected before the view is attached, show the last
+ // frame of the connected animation immediately.
+ if (curDrawable.isRunning()) {
+ curDrawable.stop();
+ }
+ curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1);
+ }
+ }
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
+ int index) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
+ refreshRoute();
+ }
+ }
+}
diff --git a/android/app/NativeActivity.java b/android/app/NativeActivity.java
new file mode 100644
index 00000000..ca663fd1
--- /dev/null
+++ b/android/app/NativeActivity.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.AttributeSet;
+import android.view.InputQueue;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+import dalvik.system.BaseDexClassLoader;
+
+import java.io.File;
+
+/**
+ * Convenience for implementing an activity that will be implemented
+ * purely in native code. That is, a game (or game-like thing). There
+ * is no need to derive from this class; you can simply declare it in your
+ * manifest, and use the NDK APIs from there.
+ *
+ * <p>A <a href="https://github.com/googlesamples/android-ndk/tree/master/native-activity">sample
+ * native activity</a> is available in the NDK samples.
+ */
+public class NativeActivity extends Activity implements SurfaceHolder.Callback2,
+ InputQueue.Callback, OnGlobalLayoutListener {
+ /**
+ * Optional meta-that can be in the manifest for this component, specifying
+ * the name of the native shared library to load. If not specified,
+ * "main" is used.
+ */
+ public static final String META_DATA_LIB_NAME = "android.app.lib_name";
+
+ /**
+ * Optional meta-that can be in the manifest for this component, specifying
+ * the name of the main entry point for this native activity in the
+ * {@link #META_DATA_LIB_NAME} native code. If not specified,
+ * "ANativeActivity_onCreate" is used.
+ */
+ public static final String META_DATA_FUNC_NAME = "android.app.func_name";
+
+ private static final String KEY_NATIVE_SAVED_STATE = "android:native_state";
+
+ private NativeContentView mNativeContentView;
+ private InputMethodManager mIMM;
+
+ private long mNativeHandle;
+
+ private InputQueue mCurInputQueue;
+ private SurfaceHolder mCurSurfaceHolder;
+
+ final int[] mLocation = new int[2];
+ int mLastContentX;
+ int mLastContentY;
+ int mLastContentWidth;
+ int mLastContentHeight;
+
+ private boolean mDispatchingUnhandledKey;
+
+ private boolean mDestroyed;
+
+ private native long loadNativeCode(String path, String funcname, MessageQueue queue,
+ String internalDataPath, String obbPath, String externalDataPath, int sdkVersion,
+ AssetManager assetMgr, byte[] savedState, ClassLoader classLoader, String libraryPath);
+ private native String getDlError();
+ private native void unloadNativeCode(long handle);
+ private native void onStartNative(long handle);
+ private native void onResumeNative(long handle);
+ private native byte[] onSaveInstanceStateNative(long handle);
+ private native void onPauseNative(long handle);
+ private native void onStopNative(long handle);
+ private native void onConfigurationChangedNative(long handle);
+ private native void onLowMemoryNative(long handle);
+ private native void onWindowFocusChangedNative(long handle, boolean focused);
+ private native void onSurfaceCreatedNative(long handle, Surface surface);
+ private native void onSurfaceChangedNative(long handle, Surface surface,
+ int format, int width, int height);
+ private native void onSurfaceRedrawNeededNative(long handle, Surface surface);
+ private native void onSurfaceDestroyedNative(long handle);
+ private native void onInputQueueCreatedNative(long handle, long queuePtr);
+ private native void onInputQueueDestroyedNative(long handle, long queuePtr);
+ private native void onContentRectChangedNative(long handle, int x, int y, int w, int h);
+
+ static class NativeContentView extends View {
+ NativeActivity mActivity;
+
+ public NativeContentView(Context context) {
+ super(context);
+ }
+
+ public NativeContentView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ String libname = "main";
+ String funcname = "ANativeActivity_onCreate";
+ ActivityInfo ai;
+
+ mIMM = getSystemService(InputMethodManager.class);
+
+ getWindow().takeSurface(this);
+ getWindow().takeInputQueue(this);
+ getWindow().setFormat(PixelFormat.RGB_565);
+ getWindow().setSoftInputMode(
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
+ | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+
+ mNativeContentView = new NativeContentView(this);
+ mNativeContentView.mActivity = this;
+ setContentView(mNativeContentView);
+ mNativeContentView.requestFocus();
+ mNativeContentView.getViewTreeObserver().addOnGlobalLayoutListener(this);
+
+ try {
+ ai = getPackageManager().getActivityInfo(
+ getIntent().getComponent(), PackageManager.GET_META_DATA);
+ if (ai.metaData != null) {
+ String ln = ai.metaData.getString(META_DATA_LIB_NAME);
+ if (ln != null) libname = ln;
+ ln = ai.metaData.getString(META_DATA_FUNC_NAME);
+ if (ln != null) funcname = ln;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException("Error getting activity info", e);
+ }
+
+ BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
+ String path = classLoader.findLibrary(libname);
+
+ if (path == null) {
+ throw new IllegalArgumentException("Unable to find native library " + libname +
+ " using classloader: " + classLoader.toString());
+ }
+
+ byte[] nativeSavedState = savedInstanceState != null
+ ? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null;
+
+ mNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(),
+ getAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()),
+ getAbsolutePath(getExternalFilesDir(null)),
+ Build.VERSION.SDK_INT, getAssets(), nativeSavedState,
+ classLoader, classLoader.getLdLibraryPath());
+
+ if (mNativeHandle == 0) {
+ throw new UnsatisfiedLinkError(
+ "Unable to load native library \"" + path + "\": " + getDlError());
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ private static String getAbsolutePath(File file) {
+ return (file != null) ? file.getAbsolutePath() : null;
+ }
+
+ @Override
+ protected void onDestroy() {
+ mDestroyed = true;
+ if (mCurSurfaceHolder != null) {
+ onSurfaceDestroyedNative(mNativeHandle);
+ mCurSurfaceHolder = null;
+ }
+ if (mCurInputQueue != null) {
+ onInputQueueDestroyedNative(mNativeHandle, mCurInputQueue.getNativePtr());
+ mCurInputQueue = null;
+ }
+ unloadNativeCode(mNativeHandle);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ onPauseNative(mNativeHandle);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ onResumeNative(mNativeHandle);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ byte[] state = onSaveInstanceStateNative(mNativeHandle);
+ if (state != null) {
+ outState.putByteArray(KEY_NATIVE_SAVED_STATE, state);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ onStartNative(mNativeHandle);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ onStopNative(mNativeHandle);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (!mDestroyed) {
+ onConfigurationChangedNative(mNativeHandle);
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ if (!mDestroyed) {
+ onLowMemoryNative(mNativeHandle);
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (!mDestroyed) {
+ onWindowFocusChangedNative(mNativeHandle, hasFocus);
+ }
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ if (!mDestroyed) {
+ mCurSurfaceHolder = holder;
+ onSurfaceCreatedNative(mNativeHandle, holder.getSurface());
+ }
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (!mDestroyed) {
+ mCurSurfaceHolder = holder;
+ onSurfaceChangedNative(mNativeHandle, holder.getSurface(), format, width, height);
+ }
+ }
+
+ public void surfaceRedrawNeeded(SurfaceHolder holder) {
+ if (!mDestroyed) {
+ mCurSurfaceHolder = holder;
+ onSurfaceRedrawNeededNative(mNativeHandle, holder.getSurface());
+ }
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mCurSurfaceHolder = null;
+ if (!mDestroyed) {
+ onSurfaceDestroyedNative(mNativeHandle);
+ }
+ }
+
+ public void onInputQueueCreated(InputQueue queue) {
+ if (!mDestroyed) {
+ mCurInputQueue = queue;
+ onInputQueueCreatedNative(mNativeHandle, queue.getNativePtr());
+ }
+ }
+
+ public void onInputQueueDestroyed(InputQueue queue) {
+ if (!mDestroyed) {
+ onInputQueueDestroyedNative(mNativeHandle, queue.getNativePtr());
+ mCurInputQueue = null;
+ }
+ }
+
+ public void onGlobalLayout() {
+ mNativeContentView.getLocationInWindow(mLocation);
+ int w = mNativeContentView.getWidth();
+ int h = mNativeContentView.getHeight();
+ if (mLocation[0] != mLastContentX || mLocation[1] != mLastContentY
+ || w != mLastContentWidth || h != mLastContentHeight) {
+ mLastContentX = mLocation[0];
+ mLastContentY = mLocation[1];
+ mLastContentWidth = w;
+ mLastContentHeight = h;
+ if (!mDestroyed) {
+ onContentRectChangedNative(mNativeHandle, mLastContentX,
+ mLastContentY, mLastContentWidth, mLastContentHeight);
+ }
+ }
+ }
+
+ void setWindowFlags(int flags, int mask) {
+ getWindow().setFlags(flags, mask);
+ }
+
+ void setWindowFormat(int format) {
+ getWindow().setFormat(format);
+ }
+
+ void showIme(int mode) {
+ mIMM.showSoftInput(mNativeContentView, mode);
+ }
+
+ void hideIme(int mode) {
+ mIMM.hideSoftInputFromWindow(mNativeContentView.getWindowToken(), mode);
+ }
+}
diff --git a/android/app/Notification.java b/android/app/Notification.java
new file mode 100644
index 00000000..841b9611
--- /dev/null
+++ b/android/app/Notification.java
@@ -0,0 +1,8530 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static com.android.internal.util.NotificationColorUtil.satisfiesTextContrast;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutInfo;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.PlayerBase;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.BadParcelableException;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.BidiFormatter;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.TextAppearanceSpan;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.NotificationHeaderView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A class that represents how a persistent notification is to be presented to
+ * the user using the {@link android.app.NotificationManager}.
+ *
+ * <p>The {@link Notification.Builder Notification.Builder} has been added to make it
+ * easier to construct Notifications.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a guide to creating notifications, read the
+ * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html">Status Bar Notifications</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class Notification implements Parcelable
+{
+ private static final String TAG = "Notification";
+
+ /**
+ * An activity that provides a user interface for adjusting notification preferences for its
+ * containing application.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String INTENT_CATEGORY_NOTIFICATION_PREFERENCES
+ = "android.intent.category.NOTIFICATION_PREFERENCES";
+
+ /**
+ * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
+ * contain a {@link NotificationChannel#getId() channel id} that can be used to narrow down
+ * what settings should be shown in the target app.
+ */
+ public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
+
+ /**
+ * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
+ * contain a {@link NotificationChannelGroup#getId() group id} that can be used to narrow down
+ * what settings should be shown in the target app.
+ */
+ public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
+
+ /**
+ * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
+ * contain the tag provided to {@link NotificationManager#notify(String, int, Notification)}
+ * that can be used to narrow down what settings should be shown in the target app.
+ */
+ public static final String EXTRA_NOTIFICATION_TAG = "android.intent.extra.NOTIFICATION_TAG";
+
+ /**
+ * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will
+ * contain the id provided to {@link NotificationManager#notify(String, int, Notification)}
+ * that can be used to narrow down what settings should be shown in the target app.
+ */
+ public static final String EXTRA_NOTIFICATION_ID = "android.intent.extra.NOTIFICATION_ID";
+
+ /**
+ * Use all default values (where applicable).
+ */
+ public static final int DEFAULT_ALL = ~0;
+
+ /**
+ * Use the default notification sound. This will ignore any given
+ * {@link #sound}.
+ *
+ * <p>
+ * A notification that is noisy is more likely to be presented as a heads-up notification.
+ * </p>
+ *
+ * @see #defaults
+ */
+
+ public static final int DEFAULT_SOUND = 1;
+
+ /**
+ * Use the default notification vibrate. This will ignore any given
+ * {@link #vibrate}. Using phone vibration requires the
+ * {@link android.Manifest.permission#VIBRATE VIBRATE} permission.
+ *
+ * <p>
+ * A notification that vibrates is more likely to be presented as a heads-up notification.
+ * </p>
+ *
+ * @see #defaults
+ */
+
+ public static final int DEFAULT_VIBRATE = 2;
+
+ /**
+ * Use the default notification lights. This will ignore the
+ * {@link #FLAG_SHOW_LIGHTS} bit, and {@link #ledARGB}, {@link #ledOffMS}, or
+ * {@link #ledOnMS}.
+ *
+ * @see #defaults
+ */
+
+ public static final int DEFAULT_LIGHTS = 4;
+
+ /**
+ * Maximum length of CharSequences accepted by Builder and friends.
+ *
+ * <p>
+ * Avoids spamming the system with overly large strings such as full e-mails.
+ */
+ private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024;
+
+ /**
+ * Maximum entries of reply text that are accepted by Builder and friends.
+ */
+ private static final int MAX_REPLY_HISTORY = 5;
+
+ /**
+ * A timestamp related to this notification, in milliseconds since the epoch.
+ *
+ * Default value: {@link System#currentTimeMillis() Now}.
+ *
+ * Choose a timestamp that will be most relevant to the user. For most finite events, this
+ * corresponds to the time the event happened (or will happen, in the case of events that have
+ * yet to occur but about which the user is being informed). Indefinite events should be
+ * timestamped according to when the activity began.
+ *
+ * Some examples:
+ *
+ * <ul>
+ * <li>Notification of a new chat message should be stamped when the message was received.</li>
+ * <li>Notification of an ongoing file download (with a progress bar, for example) should be stamped when the download started.</li>
+ * <li>Notification of a completed file download should be stamped when the download finished.</li>
+ * <li>Notification of an upcoming meeting should be stamped with the time the meeting will begin (that is, in the future).</li>
+ * <li>Notification of an ongoing stopwatch (increasing timer) should be stamped with the watch's start time.
+ * <li>Notification of an ongoing countdown timer should be stamped with the timer's end time.
+ * </ul>
+ *
+ * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not shown
+ * anymore by default and must be opted into by using
+ * {@link android.app.Notification.Builder#setShowWhen(boolean)}
+ */
+ public long when;
+
+ /**
+ * The creation time of the notification
+ */
+ private long creationTime;
+
+ /**
+ * The resource id of a drawable to use as the icon in the status bar.
+ *
+ * @deprecated Use {@link Builder#setSmallIcon(Icon)} instead.
+ */
+ @Deprecated
+ @DrawableRes
+ public int icon;
+
+ /**
+ * If the icon in the status bar is to have more than one level, you can set this. Otherwise,
+ * leave it at its default value of 0.
+ *
+ * @see android.widget.ImageView#setImageLevel
+ * @see android.graphics.drawable.Drawable#setLevel
+ */
+ public int iconLevel;
+
+ /**
+ * The number of events that this notification represents. For example, in a new mail
+ * notification, this could be the number of unread messages.
+ *
+ * The system may or may not use this field to modify the appearance of the notification.
+ * Starting with {@link android.os.Build.VERSION_CODES#O}, the number may be displayed as a
+ * badge icon in Launchers that support badging.
+ */
+ public int number = 0;
+
+ /**
+ * The intent to execute when the expanded status entry is clicked. If
+ * this is an activity, it must include the
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires
+ * that you take care of task management as described in the
+ * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back
+ * Stack</a> document. In particular, make sure to read the notification section
+ * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html#HandlingNotifications">Handling
+ * Notifications</a> for the correct ways to launch an application from a
+ * notification.
+ */
+ public PendingIntent contentIntent;
+
+ /**
+ * The intent to execute when the notification is explicitly dismissed by the user, either with
+ * the "Clear All" button or by swiping it away individually.
+ *
+ * This probably shouldn't be launching an activity since several of those will be sent
+ * at the same time.
+ */
+ public PendingIntent deleteIntent;
+
+ /**
+ * An intent to launch instead of posting the notification to the status bar.
+ *
+ * <p>
+ * The system UI may choose to display a heads-up notification, instead of
+ * launching this intent, while the user is using the device.
+ * </p>
+ *
+ * @see Notification.Builder#setFullScreenIntent
+ */
+ public PendingIntent fullScreenIntent;
+
+ /**
+ * Text that summarizes this notification for accessibility services.
+ *
+ * As of the L release, this text is no longer shown on screen, but it is still useful to
+ * accessibility services (where it serves as an audible announcement of the notification's
+ * appearance).
+ *
+ * @see #tickerView
+ */
+ public CharSequence tickerText;
+
+ /**
+ * Formerly, a view showing the {@link #tickerText}.
+ *
+ * No longer displayed in the status bar as of API 21.
+ */
+ @Deprecated
+ public RemoteViews tickerView;
+
+ /**
+ * The view that will represent this notification in the notification list (which is pulled
+ * down from the status bar).
+ *
+ * As of N, this field may be null. The notification view is determined by the inputs
+ * to {@link Notification.Builder}; a custom RemoteViews can optionally be
+ * supplied with {@link Notification.Builder#setCustomContentView(RemoteViews)}.
+ */
+ @Deprecated
+ public RemoteViews contentView;
+
+ /**
+ * A large-format version of {@link #contentView}, giving the Notification an
+ * opportunity to show more detail. The system UI may choose to show this
+ * instead of the normal content view at its discretion.
+ *
+ * As of N, this field may be null. The expanded notification view is determined by the
+ * inputs to {@link Notification.Builder}; a custom RemoteViews can optionally be
+ * supplied with {@link Notification.Builder#setCustomBigContentView(RemoteViews)}.
+ */
+ @Deprecated
+ public RemoteViews bigContentView;
+
+
+ /**
+ * A medium-format version of {@link #contentView}, providing the Notification an
+ * opportunity to add action buttons to contentView. At its discretion, the system UI may
+ * choose to show this as a heads-up notification, which will pop up so the user can see
+ * it without leaving their current activity.
+ *
+ * As of N, this field may be null. The heads-up notification view is determined by the
+ * inputs to {@link Notification.Builder}; a custom RemoteViews can optionally be
+ * supplied with {@link Notification.Builder#setCustomHeadsUpContentView(RemoteViews)}.
+ */
+ @Deprecated
+ public RemoteViews headsUpContentView;
+
+ /**
+ * A large bitmap to be shown in the notification content area.
+ *
+ * @deprecated Use {@link Builder#setLargeIcon(Icon)} instead.
+ */
+ @Deprecated
+ public Bitmap largeIcon;
+
+ /**
+ * The sound to play.
+ *
+ * <p>
+ * A notification that is noisy is more likely to be presented as a heads-up notification.
+ * </p>
+ *
+ * <p>
+ * To play the default notification sound, see {@link #defaults}.
+ * </p>
+ * @deprecated use {@link NotificationChannel#getSound()}.
+ */
+ @Deprecated
+ public Uri sound;
+
+ /**
+ * Use this constant as the value for audioStreamType to request that
+ * the default stream type for notifications be used. Currently the
+ * default stream type is {@link AudioManager#STREAM_NOTIFICATION}.
+ *
+ * @deprecated Use {@link NotificationChannel#getAudioAttributes()} instead.
+ */
+ @Deprecated
+ public static final int STREAM_DEFAULT = -1;
+
+ /**
+ * The audio stream type to use when playing the sound.
+ * Should be one of the STREAM_ constants from
+ * {@link android.media.AudioManager}.
+ *
+ * @deprecated Use {@link #audioAttributes} instead.
+ */
+ @Deprecated
+ public int audioStreamType = STREAM_DEFAULT;
+
+ /**
+ * The default value of {@link #audioAttributes}.
+ */
+ public static final AudioAttributes AUDIO_ATTRIBUTES_DEFAULT = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build();
+
+ /**
+ * The {@link AudioAttributes audio attributes} to use when playing the sound.
+ *
+ * @deprecated use {@link NotificationChannel#getAudioAttributes()} instead.
+ */
+ @Deprecated
+ public AudioAttributes audioAttributes = AUDIO_ATTRIBUTES_DEFAULT;
+
+ /**
+ * The pattern with which to vibrate.
+ *
+ * <p>
+ * To vibrate the default pattern, see {@link #defaults}.
+ * </p>
+ *
+ * @see android.os.Vibrator#vibrate(long[],int)
+ * @deprecated use {@link NotificationChannel#getVibrationPattern()}.
+ */
+ @Deprecated
+ public long[] vibrate;
+
+ /**
+ * The color of the led. The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ * @deprecated use {@link NotificationChannel#shouldShowLights()}.
+ */
+ @ColorInt
+ @Deprecated
+ public int ledARGB;
+
+ /**
+ * The number of milliseconds for the LED to be on while it's flashing.
+ * The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ * @deprecated use {@link NotificationChannel#shouldShowLights()}.
+ */
+ @Deprecated
+ public int ledOnMS;
+
+ /**
+ * The number of milliseconds for the LED to be off while it's flashing.
+ * The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ *
+ * @deprecated use {@link NotificationChannel#shouldShowLights()}.
+ */
+ @Deprecated
+ public int ledOffMS;
+
+ /**
+ * Specifies which values should be taken from the defaults.
+ * <p>
+ * To set, OR the desired from {@link #DEFAULT_SOUND},
+ * {@link #DEFAULT_VIBRATE}, {@link #DEFAULT_LIGHTS}. For all default
+ * values, use {@link #DEFAULT_ALL}.
+ * </p>
+ *
+ * @deprecated use {@link NotificationChannel#getSound()} and
+ * {@link NotificationChannel#shouldShowLights()} and
+ * {@link NotificationChannel#shouldVibrate()}.
+ */
+ @Deprecated
+ public int defaults;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if you want the LED on for this notification.
+ * <ul>
+ * <li>To turn the LED off, pass 0 in the alpha channel for colorARGB
+ * or 0 for both ledOnMS and ledOffMS.</li>
+ * <li>To turn the LED on, pass 1 for ledOnMS and 0 for ledOffMS.</li>
+ * <li>To flash the LED, pass the number of milliseconds that it should
+ * be on and off to ledOnMS and ledOffMS.</li>
+ * </ul>
+ * <p>
+ * Since hardware varies, you are not guaranteed that any of the values
+ * you pass are honored exactly. Use the system defaults (TODO) if possible
+ * because they will be set to values that work on any given hardware.
+ * <p>
+ * The alpha channel must be set for forward compatibility.
+ *
+ * @deprecated use {@link NotificationChannel#shouldShowLights()}.
+ */
+ @Deprecated
+ public static final int FLAG_SHOW_LIGHTS = 0x00000001;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if this notification is in reference to something that is ongoing,
+ * like a phone call. It should not be set if this notification is in
+ * reference to something that happened at a particular point in time,
+ * like a missed phone call.
+ */
+ public static final int FLAG_ONGOING_EVENT = 0x00000002;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that if set,
+ * the audio will be repeated until the notification is
+ * cancelled or the notification window is opened.
+ */
+ public static final int FLAG_INSISTENT = 0x00000004;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if you would only like the sound, vibrate and ticker to be played
+ * if the notification was not already showing.
+ */
+ public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if the notification should be canceled when it is clicked by the
+ * user.
+ */
+ public static final int FLAG_AUTO_CANCEL = 0x00000010;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if the notification should not be canceled when the user clicks
+ * the Clear all button.
+ */
+ public static final int FLAG_NO_CLEAR = 0x00000020;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if this notification represents a currently running service. This
+ * will normally be set for you by {@link Service#startForeground}.
+ */
+ public static final int FLAG_FOREGROUND_SERVICE = 0x00000040;
+
+ /**
+ * Obsolete flag indicating high-priority notifications; use the priority field instead.
+ *
+ * @deprecated Use {@link #priority} with a positive value.
+ */
+ @Deprecated
+ public static final int FLAG_HIGH_PRIORITY = 0x00000080;
+
+ /**
+ * Bit to be bitswise-ored into the {@link #flags} field that should be
+ * set if this notification is relevant to the current device only
+ * and it is not recommended that it bridge to other devices.
+ */
+ public static final int FLAG_LOCAL_ONLY = 0x00000100;
+
+ /**
+ * Bit to be bitswise-ored into the {@link #flags} field that should be
+ * set if this notification is the group summary for a group of notifications.
+ * Grouped notifications may display in a cluster or stack on devices which
+ * support such rendering. Requires a group key also be set using {@link Builder#setGroup}.
+ */
+ public static final int FLAG_GROUP_SUMMARY = 0x00000200;
+
+ /**
+ * Bit to be bitswise-ored into the {@link #flags} field that should be
+ * set if this notification is the group summary for an auto-group of notifications.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_AUTOGROUP_SUMMARY = 0x00000400;
+
+ /**
+ * @hide
+ */
+ public static final int FLAG_CAN_COLORIZE = 0x00000800;
+
+ public int flags;
+
+ /** @hide */
+ @IntDef({PRIORITY_DEFAULT,PRIORITY_LOW,PRIORITY_MIN,PRIORITY_HIGH,PRIORITY_MAX})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Priority {}
+
+ /**
+ * Default notification {@link #priority}. If your application does not prioritize its own
+ * notifications, use this value for all notifications.
+ *
+ * @deprecated use {@link NotificationManager#IMPORTANCE_DEFAULT} instead.
+ */
+ @Deprecated
+ public static final int PRIORITY_DEFAULT = 0;
+
+ /**
+ * Lower {@link #priority}, for items that are less important. The UI may choose to show these
+ * items smaller, or at a different position in the list, compared with your app's
+ * {@link #PRIORITY_DEFAULT} items.
+ *
+ * @deprecated use {@link NotificationManager#IMPORTANCE_LOW} instead.
+ */
+ @Deprecated
+ public static final int PRIORITY_LOW = -1;
+
+ /**
+ * Lowest {@link #priority}; these items might not be shown to the user except under special
+ * circumstances, such as detailed notification logs.
+ *
+ * @deprecated use {@link NotificationManager#IMPORTANCE_MIN} instead.
+ */
+ @Deprecated
+ public static final int PRIORITY_MIN = -2;
+
+ /**
+ * Higher {@link #priority}, for more important notifications or alerts. The UI may choose to
+ * show these items larger, or at a different position in notification lists, compared with
+ * your app's {@link #PRIORITY_DEFAULT} items.
+ *
+ * @deprecated use {@link NotificationManager#IMPORTANCE_HIGH} instead.
+ */
+ @Deprecated
+ public static final int PRIORITY_HIGH = 1;
+
+ /**
+ * Highest {@link #priority}, for your application's most important items that require the
+ * user's prompt attention or input.
+ *
+ * @deprecated use {@link NotificationManager#IMPORTANCE_HIGH} instead.
+ */
+ @Deprecated
+ public static final int PRIORITY_MAX = 2;
+
+ /**
+ * Relative priority for this notification.
+ *
+ * Priority is an indication of how much of the user's valuable attention should be consumed by
+ * this notification. Low-priority notifications may be hidden from the user in certain
+ * situations, while the user might be interrupted for a higher-priority notification. The
+ * system will make a determination about how to interpret this priority when presenting
+ * the notification.
+ *
+ * <p>
+ * A notification that is at least {@link #PRIORITY_HIGH} is more likely to be presented
+ * as a heads-up notification.
+ * </p>
+ *
+ * @deprecated use {@link NotificationChannel#getImportance()} instead.
+ */
+ @Priority
+ @Deprecated
+ public int priority;
+
+ /**
+ * Accent color (an ARGB integer like the constants in {@link android.graphics.Color})
+ * to be applied by the standard Style templates when presenting this notification.
+ *
+ * The current template design constructs a colorful header image by overlaying the
+ * {@link #icon} image (stenciled in white) atop a field of this color. Alpha components are
+ * ignored.
+ */
+ @ColorInt
+ public int color = COLOR_DEFAULT;
+
+ /**
+ * Special value of {@link #color} telling the system not to decorate this notification with
+ * any special color but instead use default colors when presenting this notification.
+ */
+ @ColorInt
+ public static final int COLOR_DEFAULT = 0; // AKA Color.TRANSPARENT
+
+ /**
+ * Special value of {@link #color} used as a place holder for an invalid color.
+ * @hide
+ */
+ @ColorInt
+ public static final int COLOR_INVALID = 1;
+
+ /**
+ * Sphere of visibility of this notification, which affects how and when the SystemUI reveals
+ * the notification's presence and contents in untrusted situations (namely, on the secure
+ * lockscreen).
+ *
+ * The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always
+ * done on Android: The notification's {@link #icon} and {@link #tickerText} (if available) are
+ * shown in all situations, but the contents are only available if the device is unlocked for
+ * the appropriate user.
+ *
+ * A more permissive policy can be expressed by {@link #VISIBILITY_PUBLIC}; such a notification
+ * can be read even in an "insecure" context (that is, above a secure lockscreen).
+ * To modify the public version of this notification—for example, to redact some portions—see
+ * {@link Builder#setPublicVersion(Notification)}.
+ *
+ * Finally, a notification can be made {@link #VISIBILITY_SECRET}, which will suppress its icon
+ * and ticker until the user has bypassed the lockscreen.
+ */
+ public @Visibility int visibility;
+
+ /** @hide */
+ @IntDef(prefix = { "VISIBILITY_" }, value = {
+ VISIBILITY_PUBLIC,
+ VISIBILITY_PRIVATE,
+ VISIBILITY_SECRET,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Visibility {}
+
+ /**
+ * Notification visibility: Show this notification in its entirety on all lockscreens.
+ *
+ * {@see #visibility}
+ */
+ public static final int VISIBILITY_PUBLIC = 1;
+
+ /**
+ * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or
+ * private information on secure lockscreens.
+ *
+ * {@see #visibility}
+ */
+ public static final int VISIBILITY_PRIVATE = 0;
+
+ /**
+ * Notification visibility: Do not reveal any part of this notification on a secure lockscreen.
+ *
+ * {@see #visibility}
+ */
+ public static final int VISIBILITY_SECRET = -1;
+
+ /**
+ * Notification category: incoming call (voice or video) or similar synchronous communication request.
+ */
+ public static final String CATEGORY_CALL = "call";
+
+ /**
+ * Notification category: incoming direct message (SMS, instant message, etc.).
+ */
+ public static final String CATEGORY_MESSAGE = "msg";
+
+ /**
+ * Notification category: asynchronous bulk message (email).
+ */
+ public static final String CATEGORY_EMAIL = "email";
+
+ /**
+ * Notification category: calendar event.
+ */
+ public static final String CATEGORY_EVENT = "event";
+
+ /**
+ * Notification category: promotion or advertisement.
+ */
+ public static final String CATEGORY_PROMO = "promo";
+
+ /**
+ * Notification category: alarm or timer.
+ */
+ public static final String CATEGORY_ALARM = "alarm";
+
+ /**
+ * Notification category: progress of a long-running background operation.
+ */
+ public static final String CATEGORY_PROGRESS = "progress";
+
+ /**
+ * Notification category: social network or sharing update.
+ */
+ public static final String CATEGORY_SOCIAL = "social";
+
+ /**
+ * Notification category: error in background operation or authentication status.
+ */
+ public static final String CATEGORY_ERROR = "err";
+
+ /**
+ * Notification category: media transport control for playback.
+ */
+ public static final String CATEGORY_TRANSPORT = "transport";
+
+ /**
+ * Notification category: system or device status update. Reserved for system use.
+ */
+ public static final String CATEGORY_SYSTEM = "sys";
+
+ /**
+ * Notification category: indication of running background service.
+ */
+ public static final String CATEGORY_SERVICE = "service";
+
+ /**
+ * Notification category: a specific, timely recommendation for a single thing.
+ * For example, a news app might want to recommend a news story it believes the user will
+ * want to read next.
+ */
+ public static final String CATEGORY_RECOMMENDATION = "recommendation";
+
+ /**
+ * Notification category: ongoing information about device or contextual status.
+ */
+ public static final String CATEGORY_STATUS = "status";
+
+ /**
+ * Notification category: user-scheduled reminder.
+ */
+ public static final String CATEGORY_REMINDER = "reminder";
+
+ /**
+ * One of the predefined notification categories (see the <code>CATEGORY_*</code> constants)
+ * that best describes this Notification. May be used by the system for ranking and filtering.
+ */
+ public String category;
+
+ private String mGroupKey;
+
+ /**
+ * Get the key used to group this notification into a cluster or stack
+ * with other notifications on devices which support such rendering.
+ */
+ public String getGroup() {
+ return mGroupKey;
+ }
+
+ private String mSortKey;
+
+ /**
+ * Get a sort key that orders this notification among other notifications from the
+ * same package. This can be useful if an external sort was already applied and an app
+ * would like to preserve this. Notifications will be sorted lexicographically using this
+ * value, although providing different priorities in addition to providing sort key may
+ * cause this value to be ignored.
+ *
+ * <p>This sort key can also be used to order members of a notification group. See
+ * {@link Builder#setGroup}.
+ *
+ * @see String#compareTo(String)
+ */
+ public String getSortKey() {
+ return mSortKey;
+ }
+
+ /**
+ * Additional semantic data to be carried around with this Notification.
+ * <p>
+ * The extras keys defined here are intended to capture the original inputs to {@link Builder}
+ * APIs, and are intended to be used by
+ * {@link android.service.notification.NotificationListenerService} implementations to extract
+ * detailed information from notification objects.
+ */
+ public Bundle extras = new Bundle();
+
+ /**
+ * All pending intents in the notification as the system needs to be able to access them but
+ * touching the extras bundle in the system process is not safe because the bundle may contain
+ * custom parcelable objects.
+ *
+ * @hide
+ */
+ public ArraySet<PendingIntent> allPendingIntents;
+
+ /**
+ * Token identifying the notification that is applying doze/bgcheck whitelisting to the
+ * pending intents inside of it, so only those will get the behavior.
+ *
+ * @hide
+ */
+ static public IBinder whitelistToken;
+
+ /**
+ * Must be set by a process to start associating tokens with Notification objects
+ * coming in to it. This is set by NotificationManagerService.
+ *
+ * @hide
+ */
+ static public IBinder processWhitelistToken;
+
+ /**
+ * {@link #extras} key: this is the title of the notification,
+ * as supplied to {@link Builder#setContentTitle(CharSequence)}.
+ */
+ public static final String EXTRA_TITLE = "android.title";
+
+ /**
+ * {@link #extras} key: this is the title of the notification when shown in expanded form,
+ * e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}.
+ */
+ public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big";
+
+ /**
+ * {@link #extras} key: this is the main text payload, as supplied to
+ * {@link Builder#setContentText(CharSequence)}.
+ */
+ public static final String EXTRA_TEXT = "android.text";
+
+ /**
+ * {@link #extras} key: this is a third line of text, as supplied to
+ * {@link Builder#setSubText(CharSequence)}.
+ */
+ public static final String EXTRA_SUB_TEXT = "android.subText";
+
+ /**
+ * {@link #extras} key: this is the remote input history, as supplied to
+ * {@link Builder#setRemoteInputHistory(CharSequence[])}.
+ *
+ * Apps can fill this through {@link Builder#setRemoteInputHistory(CharSequence[])}
+ * with the most recent inputs that have been sent through a {@link RemoteInput} of this
+ * Notification and are expected to clear it once the it is no longer relevant (e.g. for chat
+ * notifications once the other party has responded).
+ *
+ * The extra with this key is of type CharSequence[] and contains the most recent entry at
+ * the 0 index, the second most recent at the 1 index, etc.
+ *
+ * @see Builder#setRemoteInputHistory(CharSequence[])
+ */
+ public static final String EXTRA_REMOTE_INPUT_HISTORY = "android.remoteInputHistory";
+
+ /**
+ * {@link #extras} key: this is a small piece of additional text as supplied to
+ * {@link Builder#setContentInfo(CharSequence)}.
+ */
+ public static final String EXTRA_INFO_TEXT = "android.infoText";
+
+ /**
+ * {@link #extras} key: this is a line of summary information intended to be shown
+ * alongside expanded notifications, as supplied to (e.g.)
+ * {@link BigTextStyle#setSummaryText(CharSequence)}.
+ */
+ public static final String EXTRA_SUMMARY_TEXT = "android.summaryText";
+
+ /**
+ * {@link #extras} key: this is the longer text shown in the big form of a
+ * {@link BigTextStyle} notification, as supplied to
+ * {@link BigTextStyle#bigText(CharSequence)}.
+ */
+ public static final String EXTRA_BIG_TEXT = "android.bigText";
+
+ /**
+ * {@link #extras} key: this is the resource ID of the notification's main small icon, as
+ * supplied to {@link Builder#setSmallIcon(int)}.
+ *
+ * @deprecated Use {@link #getSmallIcon()}, which supports a wider variety of icon sources.
+ */
+ @Deprecated
+ public static final String EXTRA_SMALL_ICON = "android.icon";
+
+ /**
+ * {@link #extras} key: this is a bitmap to be used instead of the small icon when showing the
+ * notification payload, as
+ * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}.
+ *
+ * @deprecated Use {@link #getLargeIcon()}, which supports a wider variety of icon sources.
+ */
+ @Deprecated
+ public static final String EXTRA_LARGE_ICON = "android.largeIcon";
+
+ /**
+ * {@link #extras} key: this is a bitmap to be used instead of the one from
+ * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is
+ * shown in its expanded form, as supplied to
+ * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}.
+ */
+ public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big";
+
+ /**
+ * {@link #extras} key: this is the progress value supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS = "android.progress";
+
+ /**
+ * {@link #extras} key: this is the maximum value supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
+
+ /**
+ * {@link #extras} key: whether the progress bar is indeterminate, supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
+
+ /**
+ * {@link #extras} key: whether {@link #when} should be shown as a count-up timer (specifically
+ * a {@link android.widget.Chronometer}) instead of a timestamp, as supplied to
+ * {@link Builder#setUsesChronometer(boolean)}.
+ */
+ public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer";
+
+ /**
+ * {@link #extras} key: whether the chronometer set on the notification should count down
+ * instead of counting up. Is only relevant if key {@link #EXTRA_SHOW_CHRONOMETER} is present.
+ * This extra is a boolean. The default is false.
+ */
+ public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
+
+ /**
+ * {@link #extras} key: whether {@link #when} should be shown,
+ * as supplied to {@link Builder#setShowWhen(boolean)}.
+ */
+ public static final String EXTRA_SHOW_WHEN = "android.showWhen";
+
+ /**
+ * {@link #extras} key: this is a bitmap to be shown in {@link BigPictureStyle} expanded
+ * notifications, supplied to {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}.
+ */
+ public static final String EXTRA_PICTURE = "android.picture";
+
+ /**
+ * {@link #extras} key: An array of CharSequences to show in {@link InboxStyle} expanded
+ * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}.
+ */
+ public static final String EXTRA_TEXT_LINES = "android.textLines";
+
+ /**
+ * {@link #extras} key: A string representing the name of the specific
+ * {@link android.app.Notification.Style} used to create this notification.
+ */
+ public static final String EXTRA_TEMPLATE = "android.template";
+
+ /**
+ * {@link #extras} key: A String array containing the people that this notification relates to,
+ * each of which was supplied to {@link Builder#addPerson(String)}.
+ */
+ public static final String EXTRA_PEOPLE = "android.people";
+
+ /**
+ * Allow certain system-generated notifications to appear before the device is provisioned.
+ * Only available to notifications coming from the android package.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.NOTIFICATION_DURING_SETUP)
+ public static final String EXTRA_ALLOW_DURING_SETUP = "android.allowDuringSetup";
+
+ /**
+ * {@link #extras} key: A
+ * {@link android.content.ContentUris content URI} pointing to an image that can be displayed
+ * in the background when the notification is selected. The URI must point to an image stream
+ * suitable for passing into
+ * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
+ * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider
+ * URI used for this purpose must require no permissions to read the image data.
+ */
+ public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
+
+ /**
+ * {@link #extras} key: A
+ * {@link android.media.session.MediaSession.Token} associated with a
+ * {@link android.app.Notification.MediaStyle} notification.
+ */
+ public static final String EXTRA_MEDIA_SESSION = "android.mediaSession";
+
+ /**
+ * {@link #extras} key: the indices of actions to be shown in the compact view,
+ * as supplied to (e.g.) {@link MediaStyle#setShowActionsInCompactView(int...)}.
+ */
+ public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions";
+
+ /**
+ * {@link #extras} key: the username to be displayed for all messages sent by the user including
+ * direct replies
+ * {@link android.app.Notification.MessagingStyle} notification. This extra is a
+ * {@link CharSequence}
+ */
+ public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName";
+
+ /**
+ * {@link #extras} key: a {@link CharSequence} to be displayed as the title to a conversation
+ * represented by a {@link android.app.Notification.MessagingStyle}
+ */
+ public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
+
+ /**
+ * {@link #extras} key: an array of {@link android.app.Notification.MessagingStyle.Message}
+ * bundles provided by a
+ * {@link android.app.Notification.MessagingStyle} notification. This extra is a parcelable
+ * array of bundles.
+ */
+ public static final String EXTRA_MESSAGES = "android.messages";
+
+ /**
+ * {@link #extras} key: an array of
+ * {@link android.app.Notification.MessagingStyle#addHistoricMessage historic}
+ * {@link android.app.Notification.MessagingStyle.Message} bundles provided by a
+ * {@link android.app.Notification.MessagingStyle} notification. This extra is a parcelable
+ * array of bundles.
+ */
+ public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic";
+
+ /**
+ * {@link #extras} key: whether the notification should be colorized as
+ * supplied to {@link Builder#setColorized(boolean)}}.
+ */
+ public static final String EXTRA_COLORIZED = "android.colorized";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_CONTAINS_CUSTOM_VIEW = "android.contains.customView";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REDUCED_IMAGES = "android.reduced.images";
+
+ /**
+ * {@link #extras} key: the audio contents of this notification.
+ *
+ * This is for use when rendering the notification on an audio-focused interface;
+ * the audio contents are a complete sound sample that contains the contents/body of the
+ * notification. This may be used in substitute of a Text-to-Speech reading of the
+ * notification. For example if the notification represents a voice message this should point
+ * to the audio of that message.
+ *
+ * The data stored under this key should be a String representation of a Uri that contains the
+ * audio contents in one of the following formats: WAV, PCM 16-bit, AMR-WB.
+ *
+ * This extra is unnecessary if you are using {@code MessagingStyle} since each {@code Message}
+ * has a field for holding data URI. That field can be used for audio.
+ * See {@code Message#setData}.
+ *
+ * Example usage:
+ * <pre>
+ * {@code
+ * Notification.Builder myBuilder = (build your Notification as normal);
+ * myBuilder.getExtras().putString(EXTRA_AUDIO_CONTENTS_URI, myAudioUri.toString());
+ * }
+ * </pre>
+ */
+ public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
+
+ /** @hide */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME)
+ public static final String EXTRA_SUBSTITUTE_APP_NAME = "android.substName";
+
+ /**
+ * This is set on the notification shown by the activity manager about all apps
+ * running in the background. It indicates that the notification should be shown
+ * only if any of the given apps do not already have a {@link #FLAG_FOREGROUND_SERVICE}
+ * notification currently visible to the user. This is a string array of all
+ * package names of the apps.
+ * @hide
+ */
+ public static final String EXTRA_FOREGROUND_APPS = "android.foregroundApps";
+
+ private Icon mSmallIcon;
+ private Icon mLargeIcon;
+
+ private String mChannelId;
+ private long mTimeout;
+
+ private String mShortcutId;
+ private CharSequence mSettingsText;
+
+ /** @hide */
+ @IntDef(prefix = { "GROUP_ALERT_" }, value = {
+ GROUP_ALERT_ALL, GROUP_ALERT_CHILDREN, GROUP_ALERT_SUMMARY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GroupAlertBehavior {}
+
+ /**
+ * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all notifications in a
+ * group with sound or vibration ought to make sound or vibrate (respectively), so this
+ * notification will not be muted when it is in a group.
+ */
+ public static final int GROUP_ALERT_ALL = 0;
+
+ /**
+ * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all children
+ * notification in a group should be silenced (no sound or vibration) even if they are posted
+ * to a {@link NotificationChannel} that has sound and/or vibration. Use this constant to
+ * mute this notification if this notification is a group child. This must be applied to all
+ * children notifications you want to mute.
+ *
+ * <p> For example, you might want to use this constant if you post a number of children
+ * notifications at once (say, after a periodic sync), and only need to notify the user
+ * audibly once.
+ */
+ public static final int GROUP_ALERT_SUMMARY = 1;
+
+ /**
+ * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that the summary
+ * notification in a group should be silenced (no sound or vibration) even if they are
+ * posted to a {@link NotificationChannel} that has sound and/or vibration. Use this constant
+ * to mute this notification if this notification is a group summary.
+ *
+ * <p>For example, you might want to use this constant if only the children notifications
+ * in your group have content and the summary is only used to visually group notifications
+ * rather than to alert the user that new information is available.
+ */
+ public static final int GROUP_ALERT_CHILDREN = 2;
+
+ private int mGroupAlertBehavior = GROUP_ALERT_ALL;
+
+ /**
+ * If this notification is being shown as a badge, always show as a number.
+ */
+ public static final int BADGE_ICON_NONE = 0;
+
+ /**
+ * If this notification is being shown as a badge, use the {@link #getSmallIcon()} to
+ * represent this notification.
+ */
+ public static final int BADGE_ICON_SMALL = 1;
+
+ /**
+ * If this notification is being shown as a badge, use the {@link #getLargeIcon()} to
+ * represent this notification.
+ */
+ public static final int BADGE_ICON_LARGE = 2;
+ private int mBadgeIcon = BADGE_ICON_NONE;
+
+ /**
+ * Structure to encapsulate a named action that can be shown as part of this notification.
+ * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is
+ * selected by the user.
+ * <p>
+ * Apps should use {@link Notification.Builder#addAction(int, CharSequence, PendingIntent)}
+ * or {@link Notification.Builder#addAction(Notification.Action)}
+ * to attach actions.
+ */
+ public static class Action implements Parcelable {
+ /**
+ * {@link #extras} key: Keys to a {@link Parcelable} {@link ArrayList} of
+ * {@link RemoteInput}s.
+ *
+ * This is intended for {@link RemoteInput}s that only accept data, meaning
+ * {@link RemoteInput#getAllowFreeFormInput} is false, {@link RemoteInput#getChoices}
+ * is null or empty, and {@link RemoteInput#getAllowedDataTypes is non-null and not
+ * empty. These {@link RemoteInput}s will be ignored by devices that do not
+ * support non-text-based {@link RemoteInput}s. See {@link Builder#build}.
+ *
+ * You can test if a RemoteInput matches these constraints using
+ * {@link RemoteInput#isDataOnly}.
+ */
+ private static final String EXTRA_DATA_ONLY_INPUTS = "android.extra.DATA_ONLY_INPUTS";
+
+ private final Bundle mExtras;
+ private Icon mIcon;
+ private final RemoteInput[] mRemoteInputs;
+ private boolean mAllowGeneratedReplies = true;
+
+ /**
+ * Small icon representing the action.
+ *
+ * @deprecated Use {@link Action#getIcon()} instead.
+ */
+ @Deprecated
+ public int icon;
+
+ /**
+ * Title of the action.
+ */
+ public CharSequence title;
+
+ /**
+ * Intent to send when the user invokes this action. May be null, in which case the action
+ * may be rendered in a disabled presentation by the system UI.
+ */
+ public PendingIntent actionIntent;
+
+ private Action(Parcel in) {
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ if (mIcon.getType() == Icon.TYPE_RESOURCE) {
+ icon = mIcon.getResId();
+ }
+ }
+ title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ if (in.readInt() == 1) {
+ actionIntent = PendingIntent.CREATOR.createFromParcel(in);
+ }
+ mExtras = Bundle.setDefusable(in.readBundle(), true);
+ mRemoteInputs = in.createTypedArray(RemoteInput.CREATOR);
+ mAllowGeneratedReplies = in.readInt() == 1;
+ }
+
+ /**
+ * @deprecated Use {@link android.app.Notification.Action.Builder}.
+ */
+ @Deprecated
+ public Action(int icon, CharSequence title, PendingIntent intent) {
+ this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true);
+ }
+
+ /** Keep in sync with {@link Notification.Action.Builder#Builder(Action)}! */
+ private Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ this.mIcon = icon;
+ if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
+ this.icon = icon.getResId();
+ }
+ this.title = title;
+ this.actionIntent = intent;
+ this.mExtras = extras != null ? extras : new Bundle();
+ this.mRemoteInputs = remoteInputs;
+ this.mAllowGeneratedReplies = allowGeneratedReplies;
+ }
+
+ /**
+ * Return an icon representing the action.
+ */
+ public Icon getIcon() {
+ if (mIcon == null && icon != 0) {
+ // you snuck an icon in here without using the builder; let's try to keep it
+ mIcon = Icon.createWithResource("", icon);
+ }
+ return mIcon;
+ }
+
+ /**
+ * Get additional metadata carried around with this Action.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Return whether the platform should automatically generate possible replies for this
+ * {@link Action}
+ */
+ public boolean getAllowGeneratedReplies() {
+ return mAllowGeneratedReplies;
+ }
+
+ /**
+ * Get the list of inputs to be collected from the user when this action is sent.
+ * May return null if no remote inputs were added. Only returns inputs which accept
+ * a text input. For inputs which only accept data use {@link #getDataOnlyRemoteInputs}.
+ */
+ public RemoteInput[] getRemoteInputs() {
+ return mRemoteInputs;
+ }
+
+ /**
+ * Get the list of inputs to be collected from the user that ONLY accept data when this
+ * action is sent. These remote inputs are guaranteed to return true on a call to
+ * {@link RemoteInput#isDataOnly}.
+ *
+ * Returns null if there are no data-only remote inputs.
+ *
+ * This method exists so that legacy RemoteInput collectors that pre-date the addition
+ * of non-textual RemoteInputs do not access these remote inputs.
+ */
+ public RemoteInput[] getDataOnlyRemoteInputs() {
+ return (RemoteInput[]) mExtras.getParcelableArray(EXTRA_DATA_ONLY_INPUTS);
+ }
+
+ /**
+ * Builder class for {@link Action} objects.
+ */
+ public static final class Builder {
+ private final Icon mIcon;
+ private final CharSequence mTitle;
+ private final PendingIntent mIntent;
+ private boolean mAllowGeneratedReplies = true;
+ private final Bundle mExtras;
+ private ArrayList<RemoteInput> mRemoteInputs;
+
+ /**
+ * Construct a new builder for {@link Action} object.
+ * @param icon icon to show for this action
+ * @param title the title of the action
+ * @param intent the {@link PendingIntent} to fire when users trigger this action
+ */
+ @Deprecated
+ public Builder(int icon, CharSequence title, PendingIntent intent) {
+ this(Icon.createWithResource("", icon), title, intent);
+ }
+
+ /**
+ * Construct a new builder for {@link Action} object.
+ * @param icon icon to show for this action
+ * @param title the title of the action
+ * @param intent the {@link PendingIntent} to fire when users trigger this action
+ */
+ public Builder(Icon icon, CharSequence title, PendingIntent intent) {
+ this(icon, title, intent, new Bundle(), null, true);
+ }
+
+ /**
+ * Construct a new builder for {@link Action} object using the fields from an
+ * {@link Action}.
+ * @param action the action to read fields from.
+ */
+ public Builder(Action action) {
+ this(action.getIcon(), action.title, action.actionIntent,
+ new Bundle(action.mExtras), action.getRemoteInputs(),
+ action.getAllowGeneratedReplies());
+ }
+
+ private Builder(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ mIcon = icon;
+ mTitle = title;
+ mIntent = intent;
+ mExtras = extras;
+ if (remoteInputs != null) {
+ mRemoteInputs = new ArrayList<RemoteInput>(remoteInputs.length);
+ Collections.addAll(mRemoteInputs, remoteInputs);
+ }
+ mAllowGeneratedReplies = allowGeneratedReplies;
+ }
+
+ /**
+ * Merge additional metadata into this builder.
+ *
+ * <p>Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see Notification.Action#extras
+ */
+ public Builder addExtras(Bundle extras) {
+ if (extras != null) {
+ mExtras.putAll(extras);
+ }
+ return this;
+ }
+
+ /**
+ * Get the metadata Bundle used by this Builder.
+ *
+ * <p>The returned Bundle is shared with this Builder.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Add an input to be collected from the user when this action is sent.
+ * Response values can be retrieved from the fired intent by using the
+ * {@link RemoteInput#getResultsFromIntent} function.
+ * @param remoteInput a {@link RemoteInput} to add to the action
+ * @return this object for method chaining
+ */
+ public Builder addRemoteInput(RemoteInput remoteInput) {
+ if (mRemoteInputs == null) {
+ mRemoteInputs = new ArrayList<RemoteInput>();
+ }
+ mRemoteInputs.add(remoteInput);
+ return this;
+ }
+
+ /**
+ * Set whether the platform should automatically generate possible replies to add to
+ * {@link RemoteInput#getChoices()}. If the {@link Action} doesn't have a
+ * {@link RemoteInput}, this has no effect.
+ * @param allowGeneratedReplies {@code true} to allow generated replies, {@code false}
+ * otherwise
+ * @return this object for method chaining
+ * The default value is {@code true}
+ */
+ public Builder setAllowGeneratedReplies(boolean allowGeneratedReplies) {
+ mAllowGeneratedReplies = allowGeneratedReplies;
+ return this;
+ }
+
+ /**
+ * Apply an extender to this action builder. Extenders may be used to add
+ * metadata or change options on this builder.
+ */
+ public Builder extend(Extender extender) {
+ extender.extend(this);
+ return this;
+ }
+
+ /**
+ * Combine all of the options that have been set and return a new {@link Action}
+ * object.
+ * @return the built action
+ */
+ public Action build() {
+ ArrayList<RemoteInput> dataOnlyInputs = new ArrayList<>();
+ RemoteInput[] previousDataInputs =
+ (RemoteInput[]) mExtras.getParcelableArray(EXTRA_DATA_ONLY_INPUTS);
+ if (previousDataInputs != null) {
+ for (RemoteInput input : previousDataInputs) {
+ dataOnlyInputs.add(input);
+ }
+ }
+ List<RemoteInput> textInputs = new ArrayList<>();
+ if (mRemoteInputs != null) {
+ for (RemoteInput input : mRemoteInputs) {
+ if (input.isDataOnly()) {
+ dataOnlyInputs.add(input);
+ } else {
+ textInputs.add(input);
+ }
+ }
+ }
+ if (!dataOnlyInputs.isEmpty()) {
+ RemoteInput[] dataInputsArr =
+ dataOnlyInputs.toArray(new RemoteInput[dataOnlyInputs.size()]);
+ mExtras.putParcelableArray(EXTRA_DATA_ONLY_INPUTS, dataInputsArr);
+ }
+ RemoteInput[] textInputsArr = textInputs.isEmpty()
+ ? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
+ return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
+ mAllowGeneratedReplies);
+ }
+ }
+
+ @Override
+ public Action clone() {
+ return new Action(
+ getIcon(),
+ title,
+ actionIntent, // safe to alias
+ mExtras == null ? new Bundle() : new Bundle(mExtras),
+ getRemoteInputs(),
+ getAllowGeneratedReplies());
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ final Icon ic = getIcon();
+ if (ic != null) {
+ out.writeInt(1);
+ ic.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ TextUtils.writeToParcel(title, out, flags);
+ if (actionIntent != null) {
+ out.writeInt(1);
+ actionIntent.writeToParcel(out, flags);
+ } else {
+ out.writeInt(0);
+ }
+ out.writeBundle(mExtras);
+ out.writeTypedArray(mRemoteInputs, flags);
+ out.writeInt(mAllowGeneratedReplies ? 1 : 0);
+ }
+ public static final Parcelable.Creator<Action> CREATOR =
+ new Parcelable.Creator<Action>() {
+ public Action createFromParcel(Parcel in) {
+ return new Action(in);
+ }
+ public Action[] newArray(int size) {
+ return new Action[size];
+ }
+ };
+
+ /**
+ * Extender interface for use with {@link Builder#extend}. Extenders may be used to add
+ * metadata or change options on an action builder.
+ */
+ public interface Extender {
+ /**
+ * Apply this extender to a notification action builder.
+ * @param builder the builder to be modified.
+ * @return the build object for chaining.
+ */
+ public Builder extend(Builder builder);
+ }
+
+ /**
+ * Wearable extender for notification actions. To add extensions to an action,
+ * create a new {@link android.app.Notification.Action.WearableExtender} object using
+ * the {@code WearableExtender()} constructor and apply it to a
+ * {@link android.app.Notification.Action.Builder} using
+ * {@link android.app.Notification.Action.Builder#extend}.
+ *
+ * <pre class="prettyprint">
+ * Notification.Action action = new Notification.Action.Builder(
+ * R.drawable.archive_all, "Archive all", actionIntent)
+ * .extend(new Notification.Action.WearableExtender()
+ * .setAvailableOffline(false))
+ * .build();</pre>
+ */
+ public static final class WearableExtender implements Extender {
+ /** Notification action extra which contains wearable extensions */
+ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS";
+
+ // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options.
+ private static final String KEY_FLAGS = "flags";
+ private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel";
+ private static final String KEY_CONFIRM_LABEL = "confirmLabel";
+ private static final String KEY_CANCEL_LABEL = "cancelLabel";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_AVAILABLE_OFFLINE = 0x1;
+ private static final int FLAG_HINT_LAUNCHES_ACTIVITY = 1 << 1;
+ private static final int FLAG_HINT_DISPLAY_INLINE = 1 << 2;
+
+ // Default value for flags integer
+ private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE;
+
+ private int mFlags = DEFAULT_FLAGS;
+
+ private CharSequence mInProgressLabel;
+ private CharSequence mConfirmLabel;
+ private CharSequence mCancelLabel;
+
+ /**
+ * Create a {@link android.app.Notification.Action.WearableExtender} with default
+ * options.
+ */
+ public WearableExtender() {
+ }
+
+ /**
+ * Create a {@link android.app.Notification.Action.WearableExtender} by reading
+ * wearable options present in an existing notification action.
+ * @param action the notification action to inspect.
+ */
+ public WearableExtender(Action action) {
+ Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS);
+ if (wearableBundle != null) {
+ mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS);
+ mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL);
+ mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL);
+ mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL);
+ }
+ }
+
+ /**
+ * Apply wearable extensions to a notification action that is being built. This is
+ * typically called by the {@link android.app.Notification.Action.Builder#extend}
+ * method of {@link android.app.Notification.Action.Builder}.
+ */
+ @Override
+ public Action.Builder extend(Action.Builder builder) {
+ Bundle wearableBundle = new Bundle();
+
+ if (mFlags != DEFAULT_FLAGS) {
+ wearableBundle.putInt(KEY_FLAGS, mFlags);
+ }
+ if (mInProgressLabel != null) {
+ wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel);
+ }
+ if (mConfirmLabel != null) {
+ wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel);
+ }
+ if (mCancelLabel != null) {
+ wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel);
+ }
+
+ builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle);
+ return builder;
+ }
+
+ @Override
+ public WearableExtender clone() {
+ WearableExtender that = new WearableExtender();
+ that.mFlags = this.mFlags;
+ that.mInProgressLabel = this.mInProgressLabel;
+ that.mConfirmLabel = this.mConfirmLabel;
+ that.mCancelLabel = this.mCancelLabel;
+ return that;
+ }
+
+ /**
+ * Set whether this action is available when the wearable device is not connected to
+ * a companion device. The user can still trigger this action when the wearable device is
+ * offline, but a visual hint will indicate that the action may not be available.
+ * Defaults to true.
+ */
+ public WearableExtender setAvailableOffline(boolean availableOffline) {
+ setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline);
+ return this;
+ }
+
+ /**
+ * Get whether this action is available when the wearable device is not connected to
+ * a companion device. The user can still trigger this action when the wearable device is
+ * offline, but a visual hint will indicate that the action may not be available.
+ * Defaults to true.
+ */
+ public boolean isAvailableOffline() {
+ return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ /**
+ * Set a label to display while the wearable is preparing to automatically execute the
+ * action. This is usually a 'ing' verb ending in ellipsis like "Sending..."
+ *
+ * @param label the label to display while the action is being prepared to execute
+ * @return this object for method chaining
+ */
+ public WearableExtender setInProgressLabel(CharSequence label) {
+ mInProgressLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display while the wearable is preparing to automatically execute
+ * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..."
+ *
+ * @return the label to display while the action is being prepared to execute
+ */
+ public CharSequence getInProgressLabel() {
+ return mInProgressLabel;
+ }
+
+ /**
+ * Set a label to display to confirm that the action should be executed.
+ * This is usually an imperative verb like "Send".
+ *
+ * @param label the label to confirm the action should be executed
+ * @return this object for method chaining
+ */
+ public WearableExtender setConfirmLabel(CharSequence label) {
+ mConfirmLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display to confirm that the action should be executed.
+ * This is usually an imperative verb like "Send".
+ *
+ * @return the label to confirm the action should be executed
+ */
+ public CharSequence getConfirmLabel() {
+ return mConfirmLabel;
+ }
+
+ /**
+ * Set a label to display to cancel the action.
+ * This is usually an imperative verb, like "Cancel".
+ *
+ * @param label the label to display to cancel the action
+ * @return this object for method chaining
+ */
+ public WearableExtender setCancelLabel(CharSequence label) {
+ mCancelLabel = label;
+ return this;
+ }
+
+ /**
+ * Get the label to display to cancel the action.
+ * This is usually an imperative verb like "Cancel".
+ *
+ * @return the label to display to cancel the action
+ */
+ public CharSequence getCancelLabel() {
+ return mCancelLabel;
+ }
+
+ /**
+ * Set a hint that this Action will launch an {@link Activity} directly, telling the
+ * platform that it can generate the appropriate transitions.
+ * @param hintLaunchesActivity {@code true} if the content intent will launch
+ * an activity and transitions should be generated, false otherwise.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintLaunchesActivity(
+ boolean hintLaunchesActivity) {
+ setFlag(FLAG_HINT_LAUNCHES_ACTIVITY, hintLaunchesActivity);
+ return this;
+ }
+
+ /**
+ * Get a hint that this Action will launch an {@link Activity} directly, telling the
+ * platform that it can generate the appropriate transitions
+ * @return {@code true} if the content intent will launch an activity and transitions
+ * should be generated, false otherwise. The default value is {@code false} if this was
+ * never set.
+ */
+ public boolean getHintLaunchesActivity() {
+ return (mFlags & FLAG_HINT_LAUNCHES_ACTIVITY) != 0;
+ }
+
+ /**
+ * Set a hint that this Action should be displayed inline.
+ *
+ * @param hintDisplayInline {@code true} if action should be displayed inline, false
+ * otherwise
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintDisplayActionInline(
+ boolean hintDisplayInline) {
+ setFlag(FLAG_HINT_DISPLAY_INLINE, hintDisplayInline);
+ return this;
+ }
+
+ /**
+ * Get a hint that this Action should be displayed inline.
+ *
+ * @return {@code true} if the Action should be displayed inline, {@code false}
+ * otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintDisplayActionInline() {
+ return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
+ }
+ }
+ }
+
+ /**
+ * Array of all {@link Action} structures attached to this notification by
+ * {@link Builder#addAction(int, CharSequence, PendingIntent)}. Mostly useful for instances of
+ * {@link android.service.notification.NotificationListenerService} that provide an alternative
+ * interface for invoking actions.
+ */
+ public Action[] actions;
+
+ /**
+ * Replacement version of this notification whose content will be shown
+ * in an insecure context such as atop a secure keyguard. See {@link #visibility}
+ * and {@link #VISIBILITY_PUBLIC}.
+ */
+ public Notification publicVersion;
+
+ /**
+ * Constructs a Notification object with default values.
+ * You might want to consider using {@link Builder} instead.
+ */
+ public Notification()
+ {
+ this.when = System.currentTimeMillis();
+ this.creationTime = System.currentTimeMillis();
+ this.priority = PRIORITY_DEFAULT;
+ }
+
+ /**
+ * @hide
+ */
+ public Notification(Context context, int icon, CharSequence tickerText, long when,
+ CharSequence contentTitle, CharSequence contentText, Intent contentIntent)
+ {
+ new Builder(context)
+ .setWhen(when)
+ .setSmallIcon(icon)
+ .setTicker(tickerText)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))
+ .buildInto(this);
+ }
+
+ /**
+ * Constructs a Notification object with the information needed to
+ * have a status bar icon without the standard expanded view.
+ *
+ * @param icon The resource id of the icon to put in the status bar.
+ * @param tickerText The text that flows by in the status bar when the notification first
+ * activates.
+ * @param when The time to show in the time field. In the System.currentTimeMillis
+ * timebase.
+ *
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public Notification(int icon, CharSequence tickerText, long when)
+ {
+ this.icon = icon;
+ this.tickerText = tickerText;
+ this.when = when;
+ this.creationTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Unflatten the notification from a parcel.
+ */
+ @SuppressWarnings("unchecked")
+ public Notification(Parcel parcel) {
+ // IMPORTANT: Add unmarshaling code in readFromParcel as the pending
+ // intents in extras are always written as the last entry.
+ readFromParcelImpl(parcel);
+ // Must be read last!
+ allPendingIntents = (ArraySet<PendingIntent>) parcel.readArraySet(null);
+ }
+
+ private void readFromParcelImpl(Parcel parcel)
+ {
+ int version = parcel.readInt();
+
+ whitelistToken = parcel.readStrongBinder();
+ if (whitelistToken == null) {
+ whitelistToken = processWhitelistToken;
+ }
+ // Propagate this token to all pending intents that are unmarshalled from the parcel.
+ parcel.setClassCookie(PendingIntent.class, whitelistToken);
+
+ when = parcel.readLong();
+ creationTime = parcel.readLong();
+ if (parcel.readInt() != 0) {
+ mSmallIcon = Icon.CREATOR.createFromParcel(parcel);
+ if (mSmallIcon.getType() == Icon.TYPE_RESOURCE) {
+ icon = mSmallIcon.getResId();
+ }
+ }
+ number = parcel.readInt();
+ if (parcel.readInt() != 0) {
+ contentIntent = PendingIntent.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ deleteIntent = PendingIntent.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ tickerText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ tickerView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ contentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ mLargeIcon = Icon.CREATOR.createFromParcel(parcel);
+ }
+ defaults = parcel.readInt();
+ flags = parcel.readInt();
+ if (parcel.readInt() != 0) {
+ sound = Uri.CREATOR.createFromParcel(parcel);
+ }
+
+ audioStreamType = parcel.readInt();
+ if (parcel.readInt() != 0) {
+ audioAttributes = AudioAttributes.CREATOR.createFromParcel(parcel);
+ }
+ vibrate = parcel.createLongArray();
+ ledARGB = parcel.readInt();
+ ledOnMS = parcel.readInt();
+ ledOffMS = parcel.readInt();
+ iconLevel = parcel.readInt();
+
+ if (parcel.readInt() != 0) {
+ fullScreenIntent = PendingIntent.CREATOR.createFromParcel(parcel);
+ }
+
+ priority = parcel.readInt();
+
+ category = parcel.readString();
+
+ mGroupKey = parcel.readString();
+
+ mSortKey = parcel.readString();
+
+ extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null
+
+ actions = parcel.createTypedArray(Action.CREATOR); // may be null
+
+ if (parcel.readInt() != 0) {
+ bigContentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
+
+ if (parcel.readInt() != 0) {
+ headsUpContentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
+
+ visibility = parcel.readInt();
+
+ if (parcel.readInt() != 0) {
+ publicVersion = Notification.CREATOR.createFromParcel(parcel);
+ }
+
+ color = parcel.readInt();
+
+ if (parcel.readInt() != 0) {
+ mChannelId = parcel.readString();
+ }
+ mTimeout = parcel.readLong();
+
+ if (parcel.readInt() != 0) {
+ mShortcutId = parcel.readString();
+ }
+
+ mBadgeIcon = parcel.readInt();
+
+ if (parcel.readInt() != 0) {
+ mSettingsText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ }
+
+ mGroupAlertBehavior = parcel.readInt();
+ }
+
+ @Override
+ public Notification clone() {
+ Notification that = new Notification();
+ cloneInto(that, true);
+ return that;
+ }
+
+ /**
+ * Copy all (or if heavy is false, all except Bitmaps and RemoteViews) members
+ * of this into that.
+ * @hide
+ */
+ public void cloneInto(Notification that, boolean heavy) {
+ that.whitelistToken = this.whitelistToken;
+ that.when = this.when;
+ that.creationTime = this.creationTime;
+ that.mSmallIcon = this.mSmallIcon;
+ that.number = this.number;
+
+ // PendingIntents are global, so there's no reason (or way) to clone them.
+ that.contentIntent = this.contentIntent;
+ that.deleteIntent = this.deleteIntent;
+ that.fullScreenIntent = this.fullScreenIntent;
+
+ if (this.tickerText != null) {
+ that.tickerText = this.tickerText.toString();
+ }
+ if (heavy && this.tickerView != null) {
+ that.tickerView = this.tickerView.clone();
+ }
+ if (heavy && this.contentView != null) {
+ that.contentView = this.contentView.clone();
+ }
+ if (heavy && this.mLargeIcon != null) {
+ that.mLargeIcon = this.mLargeIcon;
+ }
+ that.iconLevel = this.iconLevel;
+ that.sound = this.sound; // android.net.Uri is immutable
+ that.audioStreamType = this.audioStreamType;
+ if (this.audioAttributes != null) {
+ that.audioAttributes = new AudioAttributes.Builder(this.audioAttributes).build();
+ }
+
+ final long[] vibrate = this.vibrate;
+ if (vibrate != null) {
+ final int N = vibrate.length;
+ final long[] vib = that.vibrate = new long[N];
+ System.arraycopy(vibrate, 0, vib, 0, N);
+ }
+
+ that.ledARGB = this.ledARGB;
+ that.ledOnMS = this.ledOnMS;
+ that.ledOffMS = this.ledOffMS;
+ that.defaults = this.defaults;
+
+ that.flags = this.flags;
+
+ that.priority = this.priority;
+
+ that.category = this.category;
+
+ that.mGroupKey = this.mGroupKey;
+
+ that.mSortKey = this.mSortKey;
+
+ if (this.extras != null) {
+ try {
+ that.extras = new Bundle(this.extras);
+ // will unparcel
+ that.extras.size();
+ } catch (BadParcelableException e) {
+ Log.e(TAG, "could not unparcel extras from notification: " + this, e);
+ that.extras = null;
+ }
+ }
+
+ if (!ArrayUtils.isEmpty(allPendingIntents)) {
+ that.allPendingIntents = new ArraySet<>(allPendingIntents);
+ }
+
+ if (this.actions != null) {
+ that.actions = new Action[this.actions.length];
+ for(int i=0; i<this.actions.length; i++) {
+ if ( this.actions[i] != null) {
+ that.actions[i] = this.actions[i].clone();
+ }
+ }
+ }
+
+ if (heavy && this.bigContentView != null) {
+ that.bigContentView = this.bigContentView.clone();
+ }
+
+ if (heavy && this.headsUpContentView != null) {
+ that.headsUpContentView = this.headsUpContentView.clone();
+ }
+
+ that.visibility = this.visibility;
+
+ if (this.publicVersion != null) {
+ that.publicVersion = new Notification();
+ this.publicVersion.cloneInto(that.publicVersion, heavy);
+ }
+
+ that.color = this.color;
+
+ that.mChannelId = this.mChannelId;
+ that.mTimeout = this.mTimeout;
+ that.mShortcutId = this.mShortcutId;
+ that.mBadgeIcon = this.mBadgeIcon;
+ that.mSettingsText = this.mSettingsText;
+ that.mGroupAlertBehavior = this.mGroupAlertBehavior;
+
+ if (!heavy) {
+ that.lightenPayload(); // will clean out extras
+ }
+ }
+
+ /**
+ * Removes heavyweight parts of the Notification object for archival or for sending to
+ * listeners when the full contents are not necessary.
+ * @hide
+ */
+ public final void lightenPayload() {
+ tickerView = null;
+ contentView = null;
+ bigContentView = null;
+ headsUpContentView = null;
+ mLargeIcon = null;
+ if (extras != null && !extras.isEmpty()) {
+ final Set<String> keyset = extras.keySet();
+ final int N = keyset.size();
+ final String[] keys = keyset.toArray(new String[N]);
+ for (int i=0; i<N; i++) {
+ final String key = keys[i];
+ if (TvExtender.EXTRA_TV_EXTENDER.equals(key)) {
+ continue;
+ }
+ final Object obj = extras.get(key);
+ if (obj != null &&
+ ( obj instanceof Parcelable
+ || obj instanceof Parcelable[]
+ || obj instanceof SparseArray
+ || obj instanceof ArrayList)) {
+ extras.remove(key);
+ }
+ }
+ }
+ }
+
+ /**
+ * Make sure this CharSequence is safe to put into a bundle, which basically
+ * means it had better not be some custom Parcelable implementation.
+ * @hide
+ */
+ public static CharSequence safeCharSequence(CharSequence cs) {
+ if (cs == null) return cs;
+ if (cs.length() > MAX_CHARSEQUENCE_LENGTH) {
+ cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH);
+ }
+ if (cs instanceof Parcelable) {
+ Log.e(TAG, "warning: " + cs.getClass().getCanonicalName()
+ + " instance is a custom Parcelable and not allowed in Notification");
+ return cs.toString();
+ }
+ return removeTextSizeSpans(cs);
+ }
+
+ private static CharSequence removeTextSizeSpans(CharSequence charSequence) {
+ if (charSequence instanceof Spanned) {
+ Spanned ss = (Spanned) charSequence;
+ Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+ SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+ for (Object span : spans) {
+ Object resultSpan = span;
+ if (resultSpan instanceof CharacterStyle) {
+ resultSpan = ((CharacterStyle) span).getUnderlying();
+ }
+ if (resultSpan instanceof TextAppearanceSpan) {
+ TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
+ resultSpan = new TextAppearanceSpan(
+ originalSpan.getFamily(),
+ originalSpan.getTextStyle(),
+ -1,
+ originalSpan.getTextColor(),
+ originalSpan.getLinkTextColor());
+ } else if (resultSpan instanceof RelativeSizeSpan
+ || resultSpan instanceof AbsoluteSizeSpan) {
+ continue;
+ } else {
+ resultSpan = span;
+ }
+ builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
+ ss.getSpanFlags(span));
+ }
+ return builder;
+ }
+ return charSequence;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this notification into a parcel.
+ */
+ public void writeToParcel(Parcel parcel, int flags) {
+ // We need to mark all pending intents getting into the notification
+ // system as being put there to later allow the notification ranker
+ // to launch them and by doing so add the app to the battery saver white
+ // list for a short period of time. The problem is that the system
+ // cannot look into the extras as there may be parcelables there that
+ // the platform does not know how to handle. To go around that we have
+ // an explicit list of the pending intents in the extras bundle.
+ final boolean collectPendingIntents = (allPendingIntents == null);
+ if (collectPendingIntents) {
+ PendingIntent.setOnMarshaledListener(
+ (PendingIntent intent, Parcel out, int outFlags) -> {
+ if (parcel == out) {
+ if (allPendingIntents == null) {
+ allPendingIntents = new ArraySet<>();
+ }
+ allPendingIntents.add(intent);
+ }
+ });
+ }
+ try {
+ // IMPORTANT: Add marshaling code in writeToParcelImpl as we
+ // want to intercept all pending events written to the parcel.
+ writeToParcelImpl(parcel, flags);
+ // Must be written last!
+ parcel.writeArraySet(allPendingIntents);
+ } finally {
+ if (collectPendingIntents) {
+ PendingIntent.setOnMarshaledListener(null);
+ }
+ }
+ }
+
+ private void writeToParcelImpl(Parcel parcel, int flags) {
+ parcel.writeInt(1);
+
+ parcel.writeStrongBinder(whitelistToken);
+ parcel.writeLong(when);
+ parcel.writeLong(creationTime);
+ if (mSmallIcon == null && icon != 0) {
+ // you snuck an icon in here without using the builder; let's try to keep it
+ mSmallIcon = Icon.createWithResource("", icon);
+ }
+ if (mSmallIcon != null) {
+ parcel.writeInt(1);
+ mSmallIcon.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeInt(number);
+ if (contentIntent != null) {
+ parcel.writeInt(1);
+ contentIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (deleteIntent != null) {
+ parcel.writeInt(1);
+ deleteIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (tickerText != null) {
+ parcel.writeInt(1);
+ TextUtils.writeToParcel(tickerText, parcel, flags);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (tickerView != null) {
+ parcel.writeInt(1);
+ tickerView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (contentView != null) {
+ parcel.writeInt(1);
+ contentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (mLargeIcon == null && largeIcon != null) {
+ // you snuck an icon in here without using the builder; let's try to keep it
+ mLargeIcon = Icon.createWithBitmap(largeIcon);
+ }
+ if (mLargeIcon != null) {
+ parcel.writeInt(1);
+ mLargeIcon.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(defaults);
+ parcel.writeInt(this.flags);
+
+ if (sound != null) {
+ parcel.writeInt(1);
+ sound.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeInt(audioStreamType);
+
+ if (audioAttributes != null) {
+ parcel.writeInt(1);
+ audioAttributes.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeLongArray(vibrate);
+ parcel.writeInt(ledARGB);
+ parcel.writeInt(ledOnMS);
+ parcel.writeInt(ledOffMS);
+ parcel.writeInt(iconLevel);
+
+ if (fullScreenIntent != null) {
+ parcel.writeInt(1);
+ fullScreenIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(priority);
+
+ parcel.writeString(category);
+
+ parcel.writeString(mGroupKey);
+
+ parcel.writeString(mSortKey);
+
+ parcel.writeBundle(extras); // null ok
+
+ parcel.writeTypedArray(actions, 0); // null ok
+
+ if (bigContentView != null) {
+ parcel.writeInt(1);
+ bigContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ if (headsUpContentView != null) {
+ parcel.writeInt(1);
+ headsUpContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(visibility);
+
+ if (publicVersion != null) {
+ parcel.writeInt(1);
+ publicVersion.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(color);
+
+ if (mChannelId != null) {
+ parcel.writeInt(1);
+ parcel.writeString(mChannelId);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeLong(mTimeout);
+
+ if (mShortcutId != null) {
+ parcel.writeInt(1);
+ parcel.writeString(mShortcutId);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(mBadgeIcon);
+
+ if (mSettingsText != null) {
+ parcel.writeInt(1);
+ TextUtils.writeToParcel(mSettingsText, parcel, flags);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(mGroupAlertBehavior);
+ }
+
+ /**
+ * Parcelable.Creator that instantiates Notification objects
+ */
+ public static final Parcelable.Creator<Notification> CREATOR
+ = new Parcelable.Creator<Notification>()
+ {
+ public Notification createFromParcel(Parcel parcel)
+ {
+ return new Notification(parcel);
+ }
+
+ public Notification[] newArray(int size)
+ {
+ return new Notification[size];
+ }
+ };
+
+ /**
+ * Sets the {@link #contentView} field to be a view with the standard "Latest Event"
+ * layout.
+ *
+ * <p>Uses the {@link #icon} and {@link #when} fields to set the icon and time fields
+ * in the view.</p>
+ * @param context The context for your application / activity.
+ * @param contentTitle The title that goes in the expanded entry.
+ * @param contentText The text that goes in the expanded entry.
+ * @param contentIntent The intent to launch when the user clicks the expanded notification.
+ * If this is an activity, it must include the
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires
+ * that you take care of task management as described in the
+ * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back
+ * Stack</a> document.
+ *
+ * @deprecated Use {@link Builder} instead.
+ * @removed
+ */
+ @Deprecated
+ public void setLatestEventInfo(Context context,
+ CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent) {
+ if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1){
+ Log.e(TAG, "setLatestEventInfo() is deprecated and you should feel deprecated.",
+ new Throwable());
+ }
+
+ if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ extras.putBoolean(EXTRA_SHOW_WHEN, true);
+ }
+
+ // ensure that any information already set directly is preserved
+ final Notification.Builder builder = new Notification.Builder(context, this);
+
+ // now apply the latestEventInfo fields
+ if (contentTitle != null) {
+ builder.setContentTitle(contentTitle);
+ }
+ if (contentText != null) {
+ builder.setContentText(contentText);
+ }
+ builder.setContentIntent(contentIntent);
+
+ builder.build(); // callers expect this notification to be ready to use
+ }
+
+ /**
+ * @hide
+ */
+ public static void addFieldsFromContext(Context context, Notification notification) {
+ addFieldsFromContext(context.getApplicationInfo(), notification);
+ }
+
+ /**
+ * @hide
+ */
+ public static void addFieldsFromContext(ApplicationInfo ai, Notification notification) {
+ notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Notification(channel=");
+ sb.append(getChannelId());
+ sb.append(" pri=");
+ sb.append(priority);
+ sb.append(" contentView=");
+ if (contentView != null) {
+ sb.append(contentView.getPackage());
+ sb.append("/0x");
+ sb.append(Integer.toHexString(contentView.getLayoutId()));
+ } else {
+ sb.append("null");
+ }
+ sb.append(" vibrate=");
+ if ((this.defaults & DEFAULT_VIBRATE) != 0) {
+ sb.append("default");
+ } else if (this.vibrate != null) {
+ int N = this.vibrate.length-1;
+ sb.append("[");
+ for (int i=0; i<N; i++) {
+ sb.append(this.vibrate[i]);
+ sb.append(',');
+ }
+ if (N != -1) {
+ sb.append(this.vibrate[N]);
+ }
+ sb.append("]");
+ } else {
+ sb.append("null");
+ }
+ sb.append(" sound=");
+ if ((this.defaults & DEFAULT_SOUND) != 0) {
+ sb.append("default");
+ } else if (this.sound != null) {
+ sb.append(this.sound.toString());
+ } else {
+ sb.append("null");
+ }
+ if (this.tickerText != null) {
+ sb.append(" tick");
+ }
+ sb.append(" defaults=0x");
+ sb.append(Integer.toHexString(this.defaults));
+ sb.append(" flags=0x");
+ sb.append(Integer.toHexString(this.flags));
+ sb.append(String.format(" color=0x%08x", this.color));
+ if (this.category != null) {
+ sb.append(" category=");
+ sb.append(this.category);
+ }
+ if (this.mGroupKey != null) {
+ sb.append(" groupKey=");
+ sb.append(this.mGroupKey);
+ }
+ if (this.mSortKey != null) {
+ sb.append(" sortKey=");
+ sb.append(this.mSortKey);
+ }
+ if (actions != null) {
+ sb.append(" actions=");
+ sb.append(actions.length);
+ }
+ sb.append(" vis=");
+ sb.append(visibilityToString(this.visibility));
+ if (this.publicVersion != null) {
+ sb.append(" publicVersion=");
+ sb.append(publicVersion.toString());
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ /**
+ * {@hide}
+ */
+ public static String visibilityToString(int vis) {
+ switch (vis) {
+ case VISIBILITY_PRIVATE:
+ return "PRIVATE";
+ case VISIBILITY_PUBLIC:
+ return "PUBLIC";
+ case VISIBILITY_SECRET:
+ return "SECRET";
+ default:
+ return "UNKNOWN(" + String.valueOf(vis) + ")";
+ }
+ }
+
+ /**
+ * {@hide}
+ */
+ public static String priorityToString(@Priority int pri) {
+ switch (pri) {
+ case PRIORITY_MIN:
+ return "MIN";
+ case PRIORITY_LOW:
+ return "LOW";
+ case PRIORITY_DEFAULT:
+ return "DEFAULT";
+ case PRIORITY_HIGH:
+ return "HIGH";
+ case PRIORITY_MAX:
+ return "MAX";
+ default:
+ return "UNKNOWN(" + String.valueOf(pri) + ")";
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean hasCompletedProgress() {
+ // not a progress notification; can't be complete
+ if (!extras.containsKey(EXTRA_PROGRESS)
+ || !extras.containsKey(EXTRA_PROGRESS_MAX)) {
+ return false;
+ }
+ // many apps use max 0 for 'indeterminate'; not complete
+ if (extras.getInt(EXTRA_PROGRESS_MAX) == 0) {
+ return false;
+ }
+ return extras.getInt(EXTRA_PROGRESS) == extras.getInt(EXTRA_PROGRESS_MAX);
+ }
+
+ /** @removed */
+ @Deprecated
+ public String getChannel() {
+ return mChannelId;
+ }
+
+ /**
+ * Returns the id of the channel this notification posts to.
+ */
+ public String getChannelId() {
+ return mChannelId;
+ }
+
+ /** @removed */
+ @Deprecated
+ public long getTimeout() {
+ return mTimeout;
+ }
+
+ /**
+ * Returns the duration from posting after which this notification should be canceled by the
+ * system, if it's not canceled already.
+ */
+ public long getTimeoutAfter() {
+ return mTimeout;
+ }
+
+ /**
+ * Returns what icon should be shown for this notification if it is being displayed in a
+ * Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE},
+ * {@link #BADGE_ICON_SMALL}, or {@link #BADGE_ICON_LARGE}.
+ */
+ public int getBadgeIconType() {
+ return mBadgeIcon;
+ }
+
+ /**
+ * Returns the {@link ShortcutInfo#getId() id} that this notification supersedes, if any.
+ *
+ * <p>Used by some Launchers that display notification content to hide shortcuts that duplicate
+ * notifications.
+ */
+ public String getShortcutId() {
+ return mShortcutId;
+ }
+
+
+ /**
+ * Returns the settings text provided to {@link Builder#setSettingsText(CharSequence)}.
+ */
+ public CharSequence getSettingsText() {
+ return mSettingsText;
+ }
+
+ /**
+ * Returns which type of notifications in a group are responsible for audibly alerting the
+ * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN},
+ * {@link #GROUP_ALERT_SUMMARY}.
+ */
+ public @GroupAlertBehavior int getGroupAlertBehavior() {
+ return mGroupAlertBehavior;
+ }
+
+ /**
+ * The small icon representing this notification in the status bar and content view.
+ *
+ * @return the small icon representing this notification.
+ *
+ * @see Builder#getSmallIcon()
+ * @see Builder#setSmallIcon(Icon)
+ */
+ public Icon getSmallIcon() {
+ return mSmallIcon;
+ }
+
+ /**
+ * Used when notifying to clean up legacy small icons.
+ * @hide
+ */
+ public void setSmallIcon(Icon icon) {
+ mSmallIcon = icon;
+ }
+
+ /**
+ * The large icon shown in this notification's content view.
+ * @see Builder#getLargeIcon()
+ * @see Builder#setLargeIcon(Icon)
+ */
+ public Icon getLargeIcon() {
+ return mLargeIcon;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isGroupSummary() {
+ return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) != 0;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isGroupChild() {
+ return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) == 0;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean suppressAlertingDueToGrouping() {
+ if (isGroupSummary()
+ && getGroupAlertBehavior() == Notification.GROUP_ALERT_CHILDREN) {
+ return true;
+ } else if (isGroupChild()
+ && getGroupAlertBehavior() == Notification.GROUP_ALERT_SUMMARY) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Builder class for {@link Notification} objects.
+ *
+ * Provides a convenient way to set the various fields of a {@link Notification} and generate
+ * content views using the platform's notification layout template. If your app supports
+ * versions of Android as old as API level 4, you can instead use
+ * {@link android.support.v4.app.NotificationCompat.Builder NotificationCompat.Builder},
+ * available in the <a href="{@docRoot}tools/extras/support-library.html">Android Support
+ * library</a>.
+ *
+ * <p>Example:
+ *
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.Builder(mContext)
+ * .setContentTitle(&quot;New mail from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap)
+ * .build();
+ * </pre>
+ */
+ public static class Builder {
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT =
+ "android.rebuild.contentViewActionCount";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT
+ = "android.rebuild.bigViewActionCount";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT
+ = "android.rebuild.hudViewActionCount";
+
+ private static final int MAX_ACTION_BUTTONS = 3;
+
+ private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY =
+ SystemProperties.getBoolean("notifications.only_title", true);
+
+ /**
+ * The lightness difference that has to be added to the primary text color to obtain the
+ * secondary text color when the background is light.
+ */
+ private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20;
+
+ /**
+ * The lightness difference that has to be added to the primary text color to obtain the
+ * secondary text color when the background is dark.
+ * A bit less then the above value, since it looks better on dark backgrounds.
+ */
+ private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10;
+
+ private Context mContext;
+ private Notification mN;
+ private Bundle mUserExtras = new Bundle();
+ private Style mStyle;
+ private ArrayList<Action> mActions = new ArrayList<Action>(MAX_ACTION_BUTTONS);
+ private ArrayList<String> mPersonList = new ArrayList<String>();
+ private NotificationColorUtil mColorUtil;
+ private boolean mIsLegacy;
+ private boolean mIsLegacyInitialized;
+
+ /**
+ * Caches a contrast-enhanced version of {@link #mCachedContrastColorIsFor}.
+ */
+ private int mCachedContrastColor = COLOR_INVALID;
+ private int mCachedContrastColorIsFor = COLOR_INVALID;
+ /**
+ * Caches a ambient version of {@link #mCachedContrastColorIsFor}.
+ */
+ private int mCachedAmbientColor = COLOR_INVALID;
+ private int mCachedAmbientColorIsFor = COLOR_INVALID;
+
+ /**
+ * Caches an instance of StandardTemplateParams. Note that this may have been used before,
+ * so make sure to call {@link StandardTemplateParams#reset()} before using it.
+ */
+ StandardTemplateParams mParams = new StandardTemplateParams();
+ private int mTextColorsAreForBackground = COLOR_INVALID;
+ private int mPrimaryTextColor = COLOR_INVALID;
+ private int mSecondaryTextColor = COLOR_INVALID;
+ private int mActionBarColor = COLOR_INVALID;
+ private int mBackgroundColor = COLOR_INVALID;
+ private int mForegroundColor = COLOR_INVALID;
+ private int mBackgroundColorHint = COLOR_INVALID;
+ /**
+ * A temporary location where actions are stored. If != null the view originally has action
+ * but doesn't have any for this inflation.
+ */
+ private ArrayList<Action> mOriginalActions;
+ private boolean mRebuildStyledRemoteViews;
+
+ private boolean mTintActionButtons;
+ private boolean mInNightMode;
+
+ /**
+ * Constructs a new Builder with the defaults:
+ *
+ * @param context
+ * A {@link Context} that will be used by the Builder to construct the
+ * RemoteViews. The Context will not be held past the lifetime of this Builder
+ * object.
+ * @param channelId
+ * The constructed Notification will be posted on this
+ * {@link NotificationChannel}. To use a NotificationChannel, it must first be
+ * created using {@link NotificationManager#createNotificationChannel}.
+ */
+ public Builder(Context context, String channelId) {
+ this(context, (Notification) null);
+ mN.mChannelId = channelId;
+ }
+
+ /**
+ * @deprecated use {@link Notification.Builder#Notification.Builder(Context, String)}
+ * instead. All posted Notifications must specify a NotificationChannel Id.
+ */
+ @Deprecated
+ public Builder(Context context) {
+ this(context, (Notification) null);
+ }
+
+ /**
+ * @hide
+ */
+ public Builder(Context context, Notification toAdopt) {
+ mContext = context;
+ Resources res = mContext.getResources();
+ mTintActionButtons = res.getBoolean(R.bool.config_tintNotificationActionButtons);
+
+ if (res.getBoolean(R.bool.config_enableNightMode)) {
+ Configuration currentConfig = res.getConfiguration();
+ mInNightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+ }
+
+ if (toAdopt == null) {
+ mN = new Notification();
+ if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ mN.extras.putBoolean(EXTRA_SHOW_WHEN, true);
+ }
+ mN.priority = PRIORITY_DEFAULT;
+ mN.visibility = VISIBILITY_PRIVATE;
+ } else {
+ mN = toAdopt;
+ if (mN.actions != null) {
+ Collections.addAll(mActions, mN.actions);
+ }
+
+ if (mN.extras.containsKey(EXTRA_PEOPLE)) {
+ Collections.addAll(mPersonList, mN.extras.getStringArray(EXTRA_PEOPLE));
+ }
+
+ if (mN.getSmallIcon() == null && mN.icon != 0) {
+ setSmallIcon(mN.icon);
+ }
+
+ if (mN.getLargeIcon() == null && mN.largeIcon != null) {
+ setLargeIcon(mN.largeIcon);
+ }
+
+ String templateClass = mN.extras.getString(EXTRA_TEMPLATE);
+ if (!TextUtils.isEmpty(templateClass)) {
+ final Class<? extends Style> styleClass
+ = getNotificationStyleClass(templateClass);
+ if (styleClass == null) {
+ Log.d(TAG, "Unknown style class: " + templateClass);
+ } else {
+ try {
+ final Constructor<? extends Style> ctor =
+ styleClass.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ final Style style = ctor.newInstance();
+ style.restoreFromExtras(mN.extras);
+
+ if (style != null) {
+ setStyle(style);
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "Could not create Style", t);
+ }
+ }
+ }
+
+ }
+ }
+
+ private NotificationColorUtil getColorUtil() {
+ if (mColorUtil == null) {
+ mColorUtil = NotificationColorUtil.getInstance(mContext);
+ }
+ return mColorUtil;
+ }
+
+ /**
+ * If this notification is duplicative of a Launcher shortcut, sets the
+ * {@link ShortcutInfo#getId() id} of the shortcut, in case the Launcher wants to hide
+ * the shortcut.
+ *
+ * This field will be ignored by Launchers that don't support badging, don't show
+ * notification content, or don't show {@link android.content.pm.ShortcutManager shortcuts}.
+ *
+ * @param shortcutId the {@link ShortcutInfo#getId() id} of the shortcut this notification
+ * supersedes
+ */
+ public Builder setShortcutId(String shortcutId) {
+ mN.mShortcutId = shortcutId;
+ return this;
+ }
+
+ /**
+ * Sets which icon to display as a badge for this notification.
+ *
+ * Must be one of {@link #BADGE_ICON_NONE}, {@link #BADGE_ICON_SMALL},
+ * {@link #BADGE_ICON_LARGE}.
+ *
+ * Note: This value might be ignored, for launchers that don't support badge icons.
+ */
+ public Builder setBadgeIconType(int icon) {
+ mN.mBadgeIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets the group alert behavior for this notification. Use this method to mute this
+ * notification if alerts for this notification's group should be handled by a different
+ * notification. This is only applicable for notifications that belong to a
+ * {@link #setGroup(String) group}. This must be called on all notifications you want to
+ * mute. For example, if you want only the summary of your group to make noise, all
+ * children in the group should have the group alert behavior {@link #GROUP_ALERT_SUMMARY}.
+ *
+ * <p> The default value is {@link #GROUP_ALERT_ALL}.</p>
+ */
+ public Builder setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) {
+ mN.mGroupAlertBehavior = groupAlertBehavior;
+ return this;
+ }
+
+ /** @removed */
+ @Deprecated
+ public Builder setChannel(String channelId) {
+ mN.mChannelId = channelId;
+ return this;
+ }
+
+ /**
+ * Specifies the channel the notification should be delivered on.
+ */
+ public Builder setChannelId(String channelId) {
+ mN.mChannelId = channelId;
+ return this;
+ }
+
+ /** @removed */
+ @Deprecated
+ public Builder setTimeout(long durationMs) {
+ mN.mTimeout = durationMs;
+ return this;
+ }
+
+ /**
+ * Specifies a duration in milliseconds after which this notification should be canceled,
+ * if it is not already canceled.
+ */
+ public Builder setTimeoutAfter(long durationMs) {
+ mN.mTimeout = durationMs;
+ return this;
+ }
+
+ /**
+ * Add a timestamp pertaining to the notification (usually the time the event occurred).
+ *
+ * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not
+ * shown anymore by default and must be opted into by using
+ * {@link android.app.Notification.Builder#setShowWhen(boolean)}
+ *
+ * @see Notification#when
+ */
+ public Builder setWhen(long when) {
+ mN.when = when;
+ return this;
+ }
+
+ /**
+ * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown
+ * in the content view.
+ * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this defaults to
+ * {@code false}. For earlier apps, the default is {@code true}.
+ */
+ public Builder setShowWhen(boolean show) {
+ mN.extras.putBoolean(EXTRA_SHOW_WHEN, show);
+ return this;
+ }
+
+ /**
+ * Show the {@link Notification#when} field as a stopwatch.
+ *
+ * Instead of presenting <code>when</code> as a timestamp, the notification will show an
+ * automatically updating display of the minutes and seconds since <code>when</code>.
+ *
+ * Useful when showing an elapsed time (like an ongoing phone call).
+ *
+ * The counter can also be set to count down to <code>when</code> when using
+ * {@link #setChronometerCountDown(boolean)}.
+ *
+ * @see android.widget.Chronometer
+ * @see Notification#when
+ * @see #setChronometerCountDown(boolean)
+ */
+ public Builder setUsesChronometer(boolean b) {
+ mN.extras.putBoolean(EXTRA_SHOW_CHRONOMETER, b);
+ return this;
+ }
+
+ /**
+ * Sets the Chronometer to count down instead of counting up.
+ *
+ * <p>This is only relevant if {@link #setUsesChronometer(boolean)} has been set to true.
+ * If it isn't set the chronometer will count up.
+ *
+ * @see #setUsesChronometer(boolean)
+ */
+ public Builder setChronometerCountDown(boolean countDown) {
+ mN.extras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, countDown);
+ return this;
+ }
+
+ /**
+ * Set the small icon resource, which will be used to represent the notification in the
+ * status bar.
+ *
+
+ * The platform template for the expanded view will draw this icon in the left, unless a
+ * {@link #setLargeIcon(Bitmap) large icon} has also been specified, in which case the small
+ * icon will be moved to the right-hand side.
+ *
+
+ * @param icon
+ * A resource ID in the application's package of the drawable to use.
+ * @see Notification#icon
+ */
+ public Builder setSmallIcon(@DrawableRes int icon) {
+ return setSmallIcon(icon != 0
+ ? Icon.createWithResource(mContext, icon)
+ : null);
+ }
+
+ /**
+ * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional
+ * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable
+ * LevelListDrawable}.
+ *
+ * @param icon A resource ID in the application's package of the drawable to use.
+ * @param level The level to use for the icon.
+ *
+ * @see Notification#icon
+ * @see Notification#iconLevel
+ */
+ public Builder setSmallIcon(@DrawableRes int icon, int level) {
+ mN.iconLevel = level;
+ return setSmallIcon(icon);
+ }
+
+ /**
+ * Set the small icon, which will be used to represent the notification in the
+ * status bar and content view (unless overriden there by a
+ * {@link #setLargeIcon(Bitmap) large icon}).
+ *
+ * @param icon An Icon object to use.
+ * @see Notification#icon
+ */
+ public Builder setSmallIcon(Icon icon) {
+ mN.setSmallIcon(icon);
+ if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
+ mN.icon = icon.getResId();
+ }
+ return this;
+ }
+
+ /**
+ * Set the first line of text in the platform notification template.
+ */
+ public Builder setContentTitle(CharSequence title) {
+ mN.extras.putCharSequence(EXTRA_TITLE, safeCharSequence(title));
+ return this;
+ }
+
+ /**
+ * Set the second line of text in the platform notification template.
+ */
+ public Builder setContentText(CharSequence text) {
+ mN.extras.putCharSequence(EXTRA_TEXT, safeCharSequence(text));
+ return this;
+ }
+
+ /**
+ * This provides some additional information that is displayed in the notification. No
+ * guarantees are given where exactly it is displayed.
+ *
+ * <p>This information should only be provided if it provides an essential
+ * benefit to the understanding of the notification. The more text you provide the
+ * less readable it becomes. For example, an email client should only provide the account
+ * name here if more than one email account has been added.</p>
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#N} this information is displayed in the
+ * notification header area.
+ *
+ * On Android versions before {@link android.os.Build.VERSION_CODES#N}
+ * this will be shown in the third line of text in the platform notification template.
+ * You should not be using {@link #setProgress(int, int, boolean)} at the
+ * same time on those versions; they occupy the same place.
+ * </p>
+ */
+ public Builder setSubText(CharSequence text) {
+ mN.extras.putCharSequence(EXTRA_SUB_TEXT, safeCharSequence(text));
+ return this;
+ }
+
+ /**
+ * Provides text that will appear as a link to your application's settings.
+ *
+ * <p>This text does not appear within notification {@link Style templates} but may
+ * appear when the user uses an affordance to learn more about the notification.
+ * Additionally, this text will not appear unless you provide a valid link target by
+ * handling {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}.
+ *
+ * <p>This text is meant to be concise description about what the user can customize
+ * when they click on this link. The recommended maximum length is 40 characters.
+ * @param text
+ * @return
+ */
+ public Builder setSettingsText(CharSequence text) {
+ mN.mSettingsText = safeCharSequence(text);
+ return this;
+ }
+
+ /**
+ * Set the remote input history.
+ *
+ * This should be set to the most recent inputs that have been sent
+ * through a {@link RemoteInput} of this Notification and cleared once the it is no
+ * longer relevant (e.g. for chat notifications once the other party has responded).
+ *
+ * The most recent input must be stored at the 0 index, the second most recent at the
+ * 1 index, etc. Note that the system will limit both how far back the inputs will be shown
+ * and how much of each individual input is shown.
+ *
+ * <p>Note: The reply text will only be shown on notifications that have least one action
+ * with a {@code RemoteInput}.</p>
+ */
+ public Builder setRemoteInputHistory(CharSequence[] text) {
+ if (text == null) {
+ mN.extras.putCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY, null);
+ } else {
+ final int N = Math.min(MAX_REPLY_HISTORY, text.length);
+ CharSequence[] safe = new CharSequence[N];
+ for (int i = 0; i < N; i++) {
+ safe[i] = safeCharSequence(text[i]);
+ }
+ mN.extras.putCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY, safe);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the number of items this notification represents. May be displayed as a badge count
+ * for Launchers that support badging.
+ */
+ public Builder setNumber(int number) {
+ mN.number = number;
+ return this;
+ }
+
+ /**
+ * A small piece of additional information pertaining to this notification.
+ *
+ * The platform template will draw this on the last line of the notification, at the far
+ * right (to the right of a smallIcon if it has been placed there).
+ *
+ * @deprecated use {@link #setSubText(CharSequence)} instead to set a text in the header.
+ * For legacy apps targeting a version below {@link android.os.Build.VERSION_CODES#N} this
+ * field will still show up, but the subtext will take precedence.
+ */
+ @Deprecated
+ public Builder setContentInfo(CharSequence info) {
+ mN.extras.putCharSequence(EXTRA_INFO_TEXT, safeCharSequence(info));
+ return this;
+ }
+
+ /**
+ * Set the progress this notification represents.
+ *
+ * The platform template will represent this using a {@link ProgressBar}.
+ */
+ public Builder setProgress(int max, int progress, boolean indeterminate) {
+ mN.extras.putInt(EXTRA_PROGRESS, progress);
+ mN.extras.putInt(EXTRA_PROGRESS_MAX, max);
+ mN.extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, indeterminate);
+ return this;
+ }
+
+ /**
+ * Supply a custom RemoteViews to use instead of the platform template.
+ *
+ * Use {@link #setCustomContentView(RemoteViews)} instead.
+ */
+ @Deprecated
+ public Builder setContent(RemoteViews views) {
+ return setCustomContentView(views);
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template.
+ *
+ * This will override the layout that would otherwise be constructed by this Builder
+ * object.
+ */
+ public Builder setCustomContentView(RemoteViews contentView) {
+ mN.contentView = contentView;
+ return this;
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template in the expanded form.
+ *
+ * This will override the expanded layout that would otherwise be constructed by this
+ * Builder object.
+ */
+ public Builder setCustomBigContentView(RemoteViews contentView) {
+ mN.bigContentView = contentView;
+ return this;
+ }
+
+ /**
+ * Supply custom RemoteViews to use instead of the platform template in the heads up dialog.
+ *
+ * This will override the heads-up layout that would otherwise be constructed by this
+ * Builder object.
+ */
+ public Builder setCustomHeadsUpContentView(RemoteViews contentView) {
+ mN.headsUpContentView = contentView;
+ return this;
+ }
+
+ /**
+ * Supply a {@link PendingIntent} to be sent when the notification is clicked.
+ *
+ * As of {@link android.os.Build.VERSION_CODES#HONEYCOMB}, if this field is unset and you
+ * have specified a custom RemoteViews with {@link #setContent(RemoteViews)}, you can use
+ * {@link RemoteViews#setOnClickPendingIntent RemoteViews.setOnClickPendingIntent(int,PendingIntent)}
+ * to assign PendingIntents to individual views in that custom layout (i.e., to create
+ * clickable buttons inside the notification view).
+ *
+ * @see Notification#contentIntent Notification.contentIntent
+ */
+ public Builder setContentIntent(PendingIntent intent) {
+ mN.contentIntent = intent;
+ return this;
+ }
+
+ /**
+ * Supply a {@link PendingIntent} to send when the notification is cleared explicitly by the user.
+ *
+ * @see Notification#deleteIntent
+ */
+ public Builder setDeleteIntent(PendingIntent intent) {
+ mN.deleteIntent = intent;
+ return this;
+ }
+
+ /**
+ * An intent to launch instead of posting the notification to the status bar.
+ * Only for use with extremely high-priority notifications demanding the user's
+ * <strong>immediate</strong> attention, such as an incoming phone call or
+ * alarm clock that the user has explicitly set to a particular time.
+ * If this facility is used for something else, please give the user an option
+ * to turn it off and use a normal notification, as this can be extremely
+ * disruptive.
+ *
+ * <p>
+ * The system UI may choose to display a heads-up notification, instead of
+ * launching this intent, while the user is using the device.
+ * </p>
+ *
+ * @param intent The pending intent to launch.
+ * @param highPriority Passing true will cause this notification to be sent
+ * even if other notifications are suppressed.
+ *
+ * @see Notification#fullScreenIntent
+ */
+ public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) {
+ mN.fullScreenIntent = intent;
+ setFlag(FLAG_HIGH_PRIORITY, highPriority);
+ return this;
+ }
+
+ /**
+ * Set the "ticker" text which is sent to accessibility services.
+ *
+ * @see Notification#tickerText
+ */
+ public Builder setTicker(CharSequence tickerText) {
+ mN.tickerText = safeCharSequence(tickerText);
+ return this;
+ }
+
+ /**
+ * Obsolete version of {@link #setTicker(CharSequence)}.
+ *
+ */
+ @Deprecated
+ public Builder setTicker(CharSequence tickerText, RemoteViews views) {
+ setTicker(tickerText);
+ // views is ignored
+ return this;
+ }
+
+ /**
+ * Add a large icon to the notification content view.
+ *
+ * In the platform template, this image will be shown on the left of the notification view
+ * in place of the {@link #setSmallIcon(Icon) small icon} (which will be placed in a small
+ * badge atop the large icon).
+ */
+ public Builder setLargeIcon(Bitmap b) {
+ return setLargeIcon(b != null ? Icon.createWithBitmap(b) : null);
+ }
+
+ /**
+ * Add a large icon to the notification content view.
+ *
+ * In the platform template, this image will be shown on the left of the notification view
+ * in place of the {@link #setSmallIcon(Icon) small icon} (which will be placed in a small
+ * badge atop the large icon).
+ */
+ public Builder setLargeIcon(Icon icon) {
+ mN.mLargeIcon = icon;
+ mN.extras.putParcelable(EXTRA_LARGE_ICON, icon);
+ return this;
+ }
+
+ /**
+ * Set the sound to play.
+ *
+ * It will be played using the {@link #AUDIO_ATTRIBUTES_DEFAULT default audio attributes}
+ * for notifications.
+ *
+ * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead.
+ */
+ @Deprecated
+ public Builder setSound(Uri sound) {
+ mN.sound = sound;
+ mN.audioAttributes = AUDIO_ATTRIBUTES_DEFAULT;
+ return this;
+ }
+
+ /**
+ * Set the sound to play, along with a specific stream on which to play it.
+ *
+ * See {@link android.media.AudioManager} for the <code>STREAM_</code> constants.
+ *
+ * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)}.
+ */
+ @Deprecated
+ public Builder setSound(Uri sound, int streamType) {
+ PlayerBase.deprecateStreamTypeForPlayback(streamType, "Notification", "setSound()");
+ mN.sound = sound;
+ mN.audioStreamType = streamType;
+ return this;
+ }
+
+ /**
+ * Set the sound to play, along with specific {@link AudioAttributes audio attributes} to
+ * use during playback.
+ *
+ * @deprecated use {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead.
+ * @see Notification#sound
+ */
+ @Deprecated
+ public Builder setSound(Uri sound, AudioAttributes audioAttributes) {
+ mN.sound = sound;
+ mN.audioAttributes = audioAttributes;
+ return this;
+ }
+
+ /**
+ * Set the vibration pattern to use.
+ *
+ * See {@link android.os.Vibrator#vibrate(long[], int)} for a discussion of the
+ * <code>pattern</code> parameter.
+ *
+ * <p>
+ * A notification that vibrates is more likely to be presented as a heads-up notification.
+ * </p>
+ *
+ * @deprecated use {@link NotificationChannel#setVibrationPattern(long[])} instead.
+ * @see Notification#vibrate
+ */
+ @Deprecated
+ public Builder setVibrate(long[] pattern) {
+ mN.vibrate = pattern;
+ return this;
+ }
+
+ /**
+ * Set the desired color for the indicator LED on the device, as well as the
+ * blink duty cycle (specified in milliseconds).
+ *
+
+ * Not all devices will honor all (or even any) of these values.
+ *
+ * @deprecated use {@link NotificationChannel#enableLights(boolean)} instead.
+ * @see Notification#ledARGB
+ * @see Notification#ledOnMS
+ * @see Notification#ledOffMS
+ */
+ @Deprecated
+ public Builder setLights(@ColorInt int argb, int onMs, int offMs) {
+ mN.ledARGB = argb;
+ mN.ledOnMS = onMs;
+ mN.ledOffMS = offMs;
+ if (onMs != 0 || offMs != 0) {
+ mN.flags |= FLAG_SHOW_LIGHTS;
+ }
+ return this;
+ }
+
+ /**
+ * Set whether this is an "ongoing" notification.
+ *
+
+ * Ongoing notifications cannot be dismissed by the user, so your application or service
+ * must take care of canceling them.
+ *
+
+ * They are typically used to indicate a background task that the user is actively engaged
+ * with (e.g., playing music) or is pending in some way and therefore occupying the device
+ * (e.g., a file download, sync operation, active network connection).
+ *
+
+ * @see Notification#FLAG_ONGOING_EVENT
+ * @see Service#setForeground(boolean)
+ */
+ public Builder setOngoing(boolean ongoing) {
+ setFlag(FLAG_ONGOING_EVENT, ongoing);
+ return this;
+ }
+
+ /**
+ * Set whether this notification should be colorized. When set, the color set with
+ * {@link #setColor(int)} will be used as the background color of this notification.
+ * <p>
+ * This should only be used for high priority ongoing tasks like navigation, an ongoing
+ * call, or other similarly high-priority events for the user.
+ * <p>
+ * For most styles, the coloring will only be applied if the notification is for a
+ * foreground service notification.
+ * However, for {@link MediaStyle} and {@link DecoratedMediaCustomViewStyle} notifications
+ * that have a media session attached there is no such requirement.
+ *
+ * @see Builder#setColor(int)
+ * @see MediaStyle#setMediaSession(MediaSession.Token)
+ */
+ public Builder setColorized(boolean colorize) {
+ mN.extras.putBoolean(EXTRA_COLORIZED, colorize);
+ return this;
+ }
+
+ /**
+ * Set this flag if you would only like the sound, vibrate
+ * and ticker to be played if the notification is not already showing.
+ *
+ * @see Notification#FLAG_ONLY_ALERT_ONCE
+ */
+ public Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
+ setFlag(FLAG_ONLY_ALERT_ONCE, onlyAlertOnce);
+ return this;
+ }
+
+ /**
+ * Make this notification automatically dismissed when the user touches it.
+ *
+ * @see Notification#FLAG_AUTO_CANCEL
+ */
+ public Builder setAutoCancel(boolean autoCancel) {
+ setFlag(FLAG_AUTO_CANCEL, autoCancel);
+ return this;
+ }
+
+ /**
+ * Set whether or not this notification should not bridge to other devices.
+ *
+ * <p>Some notifications can be bridged to other devices for remote display.
+ * This hint can be set to recommend this notification not be bridged.
+ */
+ public Builder setLocalOnly(boolean localOnly) {
+ setFlag(FLAG_LOCAL_ONLY, localOnly);
+ return this;
+ }
+
+ /**
+ * Set which notification properties will be inherited from system defaults.
+ * <p>
+ * The value should be one or more of the following fields combined with
+ * bitwise-or:
+ * {@link #DEFAULT_SOUND}, {@link #DEFAULT_VIBRATE}, {@link #DEFAULT_LIGHTS}.
+ * <p>
+ * For all default values, use {@link #DEFAULT_ALL}.
+ *
+ * @deprecated use {@link NotificationChannel#enableVibration(boolean)} and
+ * {@link NotificationChannel#enableLights(boolean)} and
+ * {@link NotificationChannel#setSound(Uri, AudioAttributes)} instead.
+ */
+ @Deprecated
+ public Builder setDefaults(int defaults) {
+ mN.defaults = defaults;
+ return this;
+ }
+
+ /**
+ * Set the priority of this notification.
+ *
+ * @see Notification#priority
+ * @deprecated use {@link NotificationChannel#setImportance(int)} instead.
+ */
+ @Deprecated
+ public Builder setPriority(@Priority int pri) {
+ mN.priority = pri;
+ return this;
+ }
+
+ /**
+ * Set the notification category.
+ *
+ * @see Notification#category
+ */
+ public Builder setCategory(String category) {
+ mN.category = category;
+ return this;
+ }
+
+ /**
+ * Add a person that is relevant to this notification.
+ *
+ * <P>
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, if this notification is of category {@link #CATEGORY_CALL}
+ * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to
+ * appear more prominently in the user interface.
+ * </P>
+ *
+ * <P>
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * </P>
+ *
+ * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+ * URIs. The path part of these URIs must exist in the contacts database, in the
+ * appropriate column, or the reference will be discarded as invalid. Telephone schema
+ * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * </P>
+ *
+ * @param uri A URI for the person.
+ * @see Notification#EXTRA_PEOPLE
+ */
+ public Builder addPerson(String uri) {
+ mPersonList.add(uri);
+ return this;
+ }
+
+ /**
+ * Set this notification to be part of a group of notifications sharing the same key.
+ * Grouped notifications may display in a cluster or stack on devices which
+ * support such rendering.
+ *
+ * <p>To make this notification the summary for its group, also call
+ * {@link #setGroupSummary}. A sort order can be specified for group members by using
+ * {@link #setSortKey}.
+ * @param groupKey The group key of the group.
+ * @return this object for method chaining
+ */
+ public Builder setGroup(String groupKey) {
+ mN.mGroupKey = groupKey;
+ return this;
+ }
+
+ /**
+ * Set this notification to be the group summary for a group of notifications.
+ * Grouped notifications may display in a cluster or stack on devices which
+ * support such rendering. If thereRequires a group key also be set using {@link #setGroup}.
+ * The group summary may be suppressed if too few notifications are included in the group.
+ * @param isGroupSummary Whether this notification should be a group summary.
+ * @return this object for method chaining
+ */
+ public Builder setGroupSummary(boolean isGroupSummary) {
+ setFlag(FLAG_GROUP_SUMMARY, isGroupSummary);
+ return this;
+ }
+
+ /**
+ * Set a sort key that orders this notification among other notifications from the
+ * same package. This can be useful if an external sort was already applied and an app
+ * would like to preserve this. Notifications will be sorted lexicographically using this
+ * value, although providing different priorities in addition to providing sort key may
+ * cause this value to be ignored.
+ *
+ * <p>This sort key can also be used to order members of a notification group. See
+ * {@link #setGroup}.
+ *
+ * @see String#compareTo(String)
+ */
+ public Builder setSortKey(String sortKey) {
+ mN.mSortKey = sortKey;
+ return this;
+ }
+
+ /**
+ * Merge additional metadata into this notification.
+ *
+ * <p>Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see Notification#extras
+ */
+ public Builder addExtras(Bundle extras) {
+ if (extras != null) {
+ mUserExtras.putAll(extras);
+ }
+ return this;
+ }
+
+ /**
+ * Set metadata for this notification.
+ *
+ * <p>A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's
+ * current contents are copied into the Notification each time {@link #build()} is
+ * called.
+ *
+ * <p>Replaces any existing extras values with those from the provided Bundle.
+ * Use {@link #addExtras} to merge in metadata instead.
+ *
+ * @see Notification#extras
+ */
+ public Builder setExtras(Bundle extras) {
+ if (extras != null) {
+ mUserExtras = extras;
+ }
+ return this;
+ }
+
+ /**
+ * Get the current metadata Bundle used by this notification Builder.
+ *
+ * <p>The returned Bundle is shared with this Builder.
+ *
+ * <p>The current contents of this Bundle are copied into the Notification each time
+ * {@link #build()} is called.
+ *
+ * @see Notification#extras
+ */
+ public Bundle getExtras() {
+ return mUserExtras;
+ }
+
+ private Bundle getAllExtras() {
+ final Bundle saveExtras = (Bundle) mUserExtras.clone();
+ saveExtras.putAll(mN.extras);
+ return saveExtras;
+ }
+
+ /**
+ * Add an action to this notification. Actions are typically displayed by
+ * the system as a button adjacent to the notification content.
+ * <p>
+ * Every action must have an icon (32dp square and matching the
+ * <a href="{@docRoot}design/style/iconography.html#action-bar">Holo
+ * Dark action bar</a> visual style), a textual label, and a {@link PendingIntent}.
+ * <p>
+ * A notification in its expanded form can display up to 3 actions, from left to right in
+ * the order they were added. Actions will not be displayed when the notification is
+ * collapsed, however, so be sure that any essential functions may be accessed by the user
+ * in some other way (for example, in the Activity pointed to by {@link #contentIntent}).
+ *
+ * @param icon Resource ID of a drawable that represents the action.
+ * @param title Text describing the action.
+ * @param intent PendingIntent to be fired when the action is invoked.
+ *
+ * @deprecated Use {@link #addAction(Action)} instead.
+ */
+ @Deprecated
+ public Builder addAction(int icon, CharSequence title, PendingIntent intent) {
+ mActions.add(new Action(icon, safeCharSequence(title), intent));
+ return this;
+ }
+
+ /**
+ * Add an action to this notification. Actions are typically displayed by
+ * the system as a button adjacent to the notification content.
+ * <p>
+ * Every action must have an icon (32dp square and matching the
+ * <a href="{@docRoot}design/style/iconography.html#action-bar">Holo
+ * Dark action bar</a> visual style), a textual label, and a {@link PendingIntent}.
+ * <p>
+ * A notification in its expanded form can display up to 3 actions, from left to right in
+ * the order they were added. Actions will not be displayed when the notification is
+ * collapsed, however, so be sure that any essential functions may be accessed by the user
+ * in some other way (for example, in the Activity pointed to by {@link #contentIntent}).
+ *
+ * @param action The action to add.
+ */
+ public Builder addAction(Action action) {
+ if (action != null) {
+ mActions.add(action);
+ }
+ return this;
+ }
+
+ /**
+ * Alter the complete list of actions attached to this notification.
+ * @see #addAction(Action).
+ *
+ * @param actions
+ * @return
+ */
+ public Builder setActions(Action... actions) {
+ mActions.clear();
+ for (int i = 0; i < actions.length; i++) {
+ if (actions[i] != null) {
+ mActions.add(actions[i]);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add a rich notification style to be applied at build time.
+ *
+ * @param style Object responsible for modifying the notification style.
+ */
+ public Builder setStyle(Style style) {
+ if (mStyle != style) {
+ mStyle = style;
+ if (mStyle != null) {
+ mStyle.setBuilder(this);
+ mN.extras.putString(EXTRA_TEMPLATE, style.getClass().getName());
+ } else {
+ mN.extras.remove(EXTRA_TEMPLATE);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Specify the value of {@link #visibility}.
+ *
+ * @return The same Builder.
+ */
+ public Builder setVisibility(@Visibility int visibility) {
+ mN.visibility = visibility;
+ return this;
+ }
+
+ /**
+ * Supply a replacement Notification whose contents should be shown in insecure contexts
+ * (i.e. atop the secure lockscreen). See {@link #visibility} and {@link #VISIBILITY_PUBLIC}.
+ * @param n A replacement notification, presumably with some or all info redacted.
+ * @return The same Builder.
+ */
+ public Builder setPublicVersion(Notification n) {
+ if (n != null) {
+ mN.publicVersion = new Notification();
+ n.cloneInto(mN.publicVersion, /*heavy=*/ true);
+ } else {
+ mN.publicVersion = null;
+ }
+ return this;
+ }
+
+ /**
+ * Apply an extender to this notification builder. Extenders may be used to add
+ * metadata or change options on this builder.
+ */
+ public Builder extend(Extender extender) {
+ extender.extend(this);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Builder setFlag(int mask, boolean value) {
+ if (value) {
+ mN.flags |= mask;
+ } else {
+ mN.flags &= ~mask;
+ }
+ return this;
+ }
+
+ /**
+ * Sets {@link Notification#color}.
+ *
+ * @param argb The accent color to use
+ *
+ * @return The same Builder.
+ */
+ public Builder setColor(@ColorInt int argb) {
+ mN.color = argb;
+ sanitizeColor();
+ return this;
+ }
+
+ private Drawable getProfileBadgeDrawable() {
+ if (mContext.getUserId() == UserHandle.USER_SYSTEM) {
+ // This user can never be a badged profile,
+ // and also includes USER_ALL system notifications.
+ return null;
+ }
+ // Note: This assumes that the current user can read the profile badge of the
+ // originating user.
+ return mContext.getPackageManager().getUserBadgeForDensityNoBackground(
+ new UserHandle(mContext.getUserId()), 0);
+ }
+
+ private Bitmap getProfileBadge() {
+ Drawable badge = getProfileBadgeDrawable();
+ if (badge == null) {
+ return null;
+ }
+ final int size = mContext.getResources().getDimensionPixelSize(
+ R.dimen.notification_badge_size);
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ badge.setBounds(0, 0, size, size);
+ badge.draw(canvas);
+ return bitmap;
+ }
+
+ private void bindProfileBadge(RemoteViews contentView) {
+ Bitmap profileBadge = getProfileBadge();
+
+ if (profileBadge != null) {
+ contentView.setImageViewBitmap(R.id.profile_badge, profileBadge);
+ contentView.setViewVisibility(R.id.profile_badge, View.VISIBLE);
+ if (isColorized()) {
+ contentView.setDrawableParameters(R.id.profile_badge, false, -1,
+ getPrimaryTextColor(), PorterDuff.Mode.SRC_ATOP, -1);
+ }
+ }
+ }
+
+ private void resetStandardTemplate(RemoteViews contentView) {
+ resetNotificationHeader(contentView);
+ resetContentMargins(contentView);
+ contentView.setViewVisibility(R.id.right_icon, View.GONE);
+ contentView.setViewVisibility(R.id.title, View.GONE);
+ contentView.setTextViewText(R.id.title, null);
+ contentView.setViewVisibility(R.id.text, View.GONE);
+ contentView.setTextViewText(R.id.text, null);
+ contentView.setViewVisibility(R.id.text_line_1, View.GONE);
+ contentView.setTextViewText(R.id.text_line_1, null);
+ }
+
+ /**
+ * Resets the notification header to its original state
+ */
+ private void resetNotificationHeader(RemoteViews contentView) {
+ // Small icon doesn't need to be reset, as it's always set. Resetting would prevent
+ // re-using the drawable when the notification is updated.
+ contentView.setBoolean(R.id.notification_header, "setExpanded", false);
+ contentView.setTextViewText(R.id.app_name_text, null);
+ contentView.setViewVisibility(R.id.chronometer, View.GONE);
+ contentView.setViewVisibility(R.id.header_text, View.GONE);
+ contentView.setTextViewText(R.id.header_text, null);
+ contentView.setViewVisibility(R.id.header_text_divider, View.GONE);
+ contentView.setViewVisibility(R.id.time_divider, View.GONE);
+ contentView.setViewVisibility(R.id.time, View.GONE);
+ contentView.setImageViewIcon(R.id.profile_badge, null);
+ contentView.setViewVisibility(R.id.profile_badge, View.GONE);
+ }
+
+ private void resetContentMargins(RemoteViews contentView) {
+ contentView.setViewLayoutMarginEndDimen(R.id.line1, 0);
+ contentView.setViewLayoutMarginEndDimen(R.id.text, 0);
+ }
+
+ private RemoteViews applyStandardTemplate(int resId) {
+ return applyStandardTemplate(resId, mParams.reset().fillTextsFrom(this));
+ }
+
+ /**
+ * @param hasProgress whether the progress bar should be shown and set
+ */
+ private RemoteViews applyStandardTemplate(int resId, boolean hasProgress) {
+ return applyStandardTemplate(resId, mParams.reset().hasProgress(hasProgress)
+ .fillTextsFrom(this));
+ }
+
+ private RemoteViews applyStandardTemplate(int resId, StandardTemplateParams p) {
+ RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId);
+
+ resetStandardTemplate(contentView);
+
+ final Bundle ex = mN.extras;
+ updateBackgroundColor(contentView);
+ bindNotificationHeader(contentView, p.ambient);
+ bindLargeIcon(contentView);
+ boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);
+ if (p.title != null) {
+ contentView.setViewVisibility(R.id.title, View.VISIBLE);
+ contentView.setTextViewText(R.id.title, processTextSpans(p.title));
+ if (!p.ambient) {
+ setTextViewColorPrimary(contentView, R.id.title);
+ }
+ contentView.setViewLayoutWidth(R.id.title, showProgress
+ ? ViewGroup.LayoutParams.WRAP_CONTENT
+ : ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ if (p.text != null) {
+ int textId = showProgress ? com.android.internal.R.id.text_line_1
+ : com.android.internal.R.id.text;
+ contentView.setTextViewText(textId, processTextSpans(p.text));
+ if (!p.ambient) {
+ setTextViewColorSecondary(contentView, textId);
+ }
+ contentView.setViewVisibility(textId, View.VISIBLE);
+ }
+
+ setContentMinHeight(contentView, showProgress || mN.hasLargeIcon());
+
+ return contentView;
+ }
+
+ private CharSequence processTextSpans(CharSequence text) {
+ if (hasForegroundColor()) {
+ return NotificationColorUtil.clearColorSpans(text);
+ }
+ return text;
+ }
+
+ private void setTextViewColorPrimary(RemoteViews contentView, int id) {
+ ensureColors();
+ contentView.setTextColor(id, mPrimaryTextColor);
+ }
+
+ private boolean hasForegroundColor() {
+ return mForegroundColor != COLOR_INVALID;
+ }
+
+ /**
+ * @return the primary text color
+ * @hide
+ */
+ @VisibleForTesting
+ public int getPrimaryTextColor() {
+ ensureColors();
+ return mPrimaryTextColor;
+ }
+
+ /**
+ * @return the secondary text color
+ * @hide
+ */
+ @VisibleForTesting
+ public int getSecondaryTextColor() {
+ ensureColors();
+ return mSecondaryTextColor;
+ }
+
+ private int getActionBarColor() {
+ ensureColors();
+ return mActionBarColor;
+ }
+
+ private int getActionBarColorDeEmphasized() {
+ int backgroundColor = getBackgroundColor();
+ return NotificationColorUtil.getShiftedColor(backgroundColor, 12);
+ }
+
+ private void setTextViewColorSecondary(RemoteViews contentView, int id) {
+ ensureColors();
+ contentView.setTextColor(id, mSecondaryTextColor);
+ }
+
+ private void ensureColors() {
+ int backgroundColor = getBackgroundColor();
+ if (mPrimaryTextColor == COLOR_INVALID
+ || mSecondaryTextColor == COLOR_INVALID
+ || mActionBarColor == COLOR_INVALID
+ || mTextColorsAreForBackground != backgroundColor) {
+ mTextColorsAreForBackground = backgroundColor;
+ if (!hasForegroundColor() || !isColorized()) {
+ mPrimaryTextColor = NotificationColorUtil.resolvePrimaryColor(mContext,
+ backgroundColor);
+ mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(mContext,
+ backgroundColor);
+ if (backgroundColor != COLOR_DEFAULT
+ && (mBackgroundColorHint != COLOR_INVALID || isColorized())) {
+ mPrimaryTextColor = NotificationColorUtil.findAlphaToMeetContrast(
+ mPrimaryTextColor, backgroundColor, 4.5);
+ mSecondaryTextColor = NotificationColorUtil.findAlphaToMeetContrast(
+ mSecondaryTextColor, backgroundColor, 4.5);
+ }
+ } else {
+ double backLum = NotificationColorUtil.calculateLuminance(backgroundColor);
+ double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor);
+ double contrast = NotificationColorUtil.calculateContrast(mForegroundColor,
+ backgroundColor);
+ // We only respect the given colors if worst case Black or White still has
+ // contrast
+ boolean backgroundLight = backLum > textLum
+ && satisfiesTextContrast(backgroundColor, Color.BLACK)
+ || backLum <= textLum
+ && !satisfiesTextContrast(backgroundColor, Color.WHITE);
+ if (contrast < 4.5f) {
+ if (backgroundLight) {
+ mSecondaryTextColor = NotificationColorUtil.findContrastColor(
+ mForegroundColor,
+ backgroundColor,
+ true /* findFG */,
+ 4.5f);
+ mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
+ mSecondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT);
+ } else {
+ mSecondaryTextColor =
+ NotificationColorUtil.findContrastColorAgainstDark(
+ mForegroundColor,
+ backgroundColor,
+ true /* findFG */,
+ 4.5f);
+ mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
+ mSecondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK);
+ }
+ } else {
+ mPrimaryTextColor = mForegroundColor;
+ mSecondaryTextColor = NotificationColorUtil.changeColorLightness(
+ mPrimaryTextColor, backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT
+ : LIGHTNESS_TEXT_DIFFERENCE_DARK);
+ if (NotificationColorUtil.calculateContrast(mSecondaryTextColor,
+ backgroundColor) < 4.5f) {
+ // oh well the secondary is not good enough
+ if (backgroundLight) {
+ mSecondaryTextColor = NotificationColorUtil.findContrastColor(
+ mSecondaryTextColor,
+ backgroundColor,
+ true /* findFG */,
+ 4.5f);
+ } else {
+ mSecondaryTextColor
+ = NotificationColorUtil.findContrastColorAgainstDark(
+ mSecondaryTextColor,
+ backgroundColor,
+ true /* findFG */,
+ 4.5f);
+ }
+ mPrimaryTextColor = NotificationColorUtil.changeColorLightness(
+ mSecondaryTextColor, backgroundLight
+ ? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT
+ : -LIGHTNESS_TEXT_DIFFERENCE_DARK);
+ }
+ }
+ }
+ mActionBarColor = NotificationColorUtil.resolveActionBarColor(mContext,
+ backgroundColor);
+ }
+ }
+
+ private void updateBackgroundColor(RemoteViews contentView) {
+ if (isColorized()) {
+ contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundColor",
+ getBackgroundColor());
+ } else {
+ // Clear it!
+ contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundResource",
+ 0);
+ }
+ }
+
+ /**
+ * @param remoteView the remote view to update the minheight in
+ * @param hasMinHeight does it have a mimHeight
+ * @hide
+ */
+ void setContentMinHeight(RemoteViews remoteView, boolean hasMinHeight) {
+ int minHeight = 0;
+ if (hasMinHeight) {
+ // we need to set the minHeight of the notification
+ minHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_min_content_height);
+ }
+ remoteView.setInt(R.id.notification_main_column, "setMinimumHeight", minHeight);
+ }
+
+ private boolean handleProgressBar(boolean hasProgress, RemoteViews contentView, Bundle ex) {
+ final int max = ex.getInt(EXTRA_PROGRESS_MAX, 0);
+ final int progress = ex.getInt(EXTRA_PROGRESS, 0);
+ final boolean ind = ex.getBoolean(EXTRA_PROGRESS_INDETERMINATE);
+ if (hasProgress && (max != 0 || ind)) {
+ contentView.setViewVisibility(com.android.internal.R.id.progress, View.VISIBLE);
+ contentView.setProgressBar(
+ R.id.progress, max, progress, ind);
+ contentView.setProgressBackgroundTintList(
+ R.id.progress, ColorStateList.valueOf(mContext.getColor(
+ R.color.notification_progress_background_color)));
+ if (mN.color != COLOR_DEFAULT) {
+ ColorStateList colorStateList = ColorStateList.valueOf(resolveContrastColor());
+ contentView.setProgressTintList(R.id.progress, colorStateList);
+ contentView.setProgressIndeterminateTintList(R.id.progress, colorStateList);
+ }
+ return true;
+ } else {
+ contentView.setViewVisibility(R.id.progress, View.GONE);
+ return false;
+ }
+ }
+
+ private void bindLargeIcon(RemoteViews contentView) {
+ if (mN.mLargeIcon == null && mN.largeIcon != null) {
+ mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon);
+ }
+ if (mN.mLargeIcon != null) {
+ contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
+ contentView.setImageViewIcon(R.id.right_icon, mN.mLargeIcon);
+ processLargeLegacyIcon(mN.mLargeIcon, contentView);
+ int endMargin = R.dimen.notification_content_picture_margin;
+ contentView.setViewLayoutMarginEndDimen(R.id.line1, endMargin);
+ contentView.setViewLayoutMarginEndDimen(R.id.text, endMargin);
+ contentView.setViewLayoutMarginEndDimen(R.id.progress, endMargin);
+ // Bind the reply action
+ Action action = findReplyAction();
+ contentView.setViewVisibility(R.id.reply_icon_action, action != null
+ ? View.VISIBLE
+ : View.GONE);
+
+ if (action != null) {
+ int contrastColor = resolveContrastColor();
+ contentView.setDrawableParameters(R.id.reply_icon_action,
+ true /* targetBackground */,
+ -1,
+ contrastColor,
+ PorterDuff.Mode.SRC_ATOP, -1);
+ int iconColor = NotificationColorUtil.isColorLight(contrastColor)
+ ? Color.BLACK : Color.WHITE;
+ contentView.setDrawableParameters(R.id.reply_icon_action,
+ false /* targetBackground */,
+ -1,
+ iconColor,
+ PorterDuff.Mode.SRC_ATOP, -1);
+ contentView.setOnClickPendingIntent(R.id.right_icon,
+ action.actionIntent);
+ contentView.setOnClickPendingIntent(R.id.reply_icon_action,
+ action.actionIntent);
+ contentView.setRemoteInputs(R.id.right_icon, action.mRemoteInputs);
+ contentView.setRemoteInputs(R.id.reply_icon_action, action.mRemoteInputs);
+
+ }
+ }
+ contentView.setViewVisibility(R.id.right_icon_container, mN.mLargeIcon != null
+ ? View.VISIBLE
+ : View.GONE);
+ }
+
+ private Action findReplyAction() {
+ ArrayList<Action> actions = mActions;
+ if (mOriginalActions != null) {
+ actions = mOriginalActions;
+ }
+ int numActions = actions.size();
+ for (int i = 0; i < numActions; i++) {
+ Action action = actions.get(i);
+ if (hasValidRemoteInput(action)) {
+ return action;
+ }
+ }
+ return null;
+ }
+
+ private void bindNotificationHeader(RemoteViews contentView, boolean ambient) {
+ bindSmallIcon(contentView, ambient);
+ bindHeaderAppName(contentView, ambient);
+ if (!ambient) {
+ // Ambient view does not have these
+ bindHeaderText(contentView);
+ bindHeaderChronometerAndTime(contentView);
+ bindProfileBadge(contentView);
+ }
+ bindExpandButton(contentView);
+ }
+
+ private void bindExpandButton(RemoteViews contentView) {
+ int color = getPrimaryHighlightColor();
+ contentView.setDrawableParameters(R.id.expand_button, false, -1, color,
+ PorterDuff.Mode.SRC_ATOP, -1);
+ contentView.setInt(R.id.notification_header, "setOriginalNotificationColor",
+ color);
+ }
+
+ /**
+ * @return the color that is used as the first primary highlight color. This is applied
+ * in several places like the action buttons or the app name in the header.
+ */
+ private int getPrimaryHighlightColor() {
+ return isColorized() ? getPrimaryTextColor() : resolveContrastColor();
+ }
+
+ private void bindHeaderChronometerAndTime(RemoteViews contentView) {
+ if (showsTimeOrChronometer()) {
+ contentView.setViewVisibility(R.id.time_divider, View.VISIBLE);
+ setTextViewColorSecondary(contentView, R.id.time_divider);
+ if (mN.extras.getBoolean(EXTRA_SHOW_CHRONOMETER)) {
+ contentView.setViewVisibility(R.id.chronometer, View.VISIBLE);
+ contentView.setLong(R.id.chronometer, "setBase",
+ mN.when + (SystemClock.elapsedRealtime() - System.currentTimeMillis()));
+ contentView.setBoolean(R.id.chronometer, "setStarted", true);
+ boolean countsDown = mN.extras.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN);
+ contentView.setChronometerCountDown(R.id.chronometer, countsDown);
+ setTextViewColorSecondary(contentView, R.id.chronometer);
+ } else {
+ contentView.setViewVisibility(R.id.time, View.VISIBLE);
+ contentView.setLong(R.id.time, "setTime", mN.when);
+ setTextViewColorSecondary(contentView, R.id.time);
+ }
+ } else {
+ // We still want a time to be set but gone, such that we can show and hide it
+ // on demand in case it's a child notification without anything in the header
+ contentView.setLong(R.id.time, "setTime", mN.when != 0 ? mN.when : mN.creationTime);
+ }
+ }
+
+ private void bindHeaderText(RemoteViews contentView) {
+ CharSequence headerText = mN.extras.getCharSequence(EXTRA_SUB_TEXT);
+ if (headerText == null && mStyle != null && mStyle.mSummaryTextSet
+ && mStyle.hasSummaryInHeader()) {
+ headerText = mStyle.mSummaryText;
+ }
+ if (headerText == null
+ && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N
+ && mN.extras.getCharSequence(EXTRA_INFO_TEXT) != null) {
+ headerText = mN.extras.getCharSequence(EXTRA_INFO_TEXT);
+ }
+ if (headerText != null) {
+ // TODO: Remove the span entirely to only have the string with propper formating.
+ contentView.setTextViewText(R.id.header_text, processTextSpans(
+ processLegacyText(headerText)));
+ setTextViewColorSecondary(contentView, R.id.header_text);
+ contentView.setViewVisibility(R.id.header_text, View.VISIBLE);
+ contentView.setViewVisibility(R.id.header_text_divider, View.VISIBLE);
+ setTextViewColorSecondary(contentView, R.id.header_text_divider);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public String loadHeaderAppName() {
+ CharSequence name = null;
+ final PackageManager pm = mContext.getPackageManager();
+ if (mN.extras.containsKey(EXTRA_SUBSTITUTE_APP_NAME)) {
+ // only system packages which lump together a bunch of unrelated stuff
+ // may substitute a different name to make the purpose of the
+ // notification more clear. the correct package label should always
+ // be accessible via SystemUI.
+ final String pkg = mContext.getPackageName();
+ final String subName = mN.extras.getString(EXTRA_SUBSTITUTE_APP_NAME);
+ if (PackageManager.PERMISSION_GRANTED == pm.checkPermission(
+ android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME, pkg)) {
+ name = subName;
+ } else {
+ Log.w(TAG, "warning: pkg "
+ + pkg + " attempting to substitute app name '" + subName
+ + "' without holding perm "
+ + android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME);
+ }
+ }
+ if (TextUtils.isEmpty(name)) {
+ name = pm.getApplicationLabel(mContext.getApplicationInfo());
+ }
+ if (TextUtils.isEmpty(name)) {
+ // still nothing?
+ return null;
+ }
+
+ return String.valueOf(name);
+ }
+ private void bindHeaderAppName(RemoteViews contentView, boolean ambient) {
+ contentView.setTextViewText(R.id.app_name_text, loadHeaderAppName());
+ if (isColorized() && !ambient) {
+ setTextViewColorPrimary(contentView, R.id.app_name_text);
+ } else {
+ contentView.setTextColor(R.id.app_name_text,
+ ambient ? resolveAmbientColor() : resolveContrastColor());
+ }
+ }
+
+ private void bindSmallIcon(RemoteViews contentView, boolean ambient) {
+ if (mN.mSmallIcon == null && mN.icon != 0) {
+ mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon);
+ }
+ contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon);
+ contentView.setDrawableParameters(R.id.icon, false /* targetBackground */,
+ -1 /* alpha */, -1 /* colorFilter */, null /* mode */, mN.iconLevel);
+ processSmallIconColor(mN.mSmallIcon, contentView, ambient);
+ }
+
+ /**
+ * @return true if the built notification will show the time or the chronometer; false
+ * otherwise
+ */
+ private boolean showsTimeOrChronometer() {
+ return mN.showsTime() || mN.showsChronometer();
+ }
+
+ private void resetStandardTemplateWithActions(RemoteViews big) {
+ // actions_container is only reset when there are no actions to avoid focus issues with
+ // remote inputs.
+ big.setViewVisibility(R.id.actions, View.GONE);
+ big.removeAllViews(R.id.actions);
+
+ big.setViewVisibility(R.id.notification_material_reply_container, View.GONE);
+ big.setTextViewText(R.id.notification_material_reply_text_1, null);
+
+ big.setViewVisibility(R.id.notification_material_reply_text_2, View.GONE);
+ big.setTextViewText(R.id.notification_material_reply_text_2, null);
+ big.setViewVisibility(R.id.notification_material_reply_text_3, View.GONE);
+ big.setTextViewText(R.id.notification_material_reply_text_3, null);
+
+ big.setViewLayoutMarginBottomDimen(R.id.notification_action_list_margin_target, 0);
+ }
+
+ private RemoteViews applyStandardTemplateWithActions(int layoutId) {
+ return applyStandardTemplateWithActions(layoutId, mParams.reset().fillTextsFrom(this));
+ }
+
+ private RemoteViews applyStandardTemplateWithActions(int layoutId,
+ StandardTemplateParams p) {
+ RemoteViews big = applyStandardTemplate(layoutId, p);
+
+ resetStandardTemplateWithActions(big);
+
+ boolean validRemoteInput = false;
+
+ int N = mActions.size();
+ boolean emphazisedMode = mN.fullScreenIntent != null && !p.ambient;
+ big.setBoolean(R.id.actions, "setEmphasizedMode", emphazisedMode);
+ if (N > 0) {
+ big.setViewVisibility(R.id.actions_container, View.VISIBLE);
+ big.setViewVisibility(R.id.actions, View.VISIBLE);
+ if (p.ambient) {
+ big.setInt(R.id.actions, "setBackgroundColor", Color.TRANSPARENT);
+ } else if (isColorized()) {
+ big.setInt(R.id.actions, "setBackgroundColor", getActionBarColor());
+ } else {
+ big.setInt(R.id.actions, "setBackgroundColor", mContext.getColor(
+ R.color.notification_action_list));
+ }
+ big.setViewLayoutMarginBottomDimen(R.id.notification_action_list_margin_target,
+ R.dimen.notification_action_list_height);
+ if (N>MAX_ACTION_BUTTONS) N=MAX_ACTION_BUTTONS;
+ for (int i=0; i<N; i++) {
+ Action action = mActions.get(i);
+ validRemoteInput |= hasValidRemoteInput(action);
+
+ final RemoteViews button = generateActionButton(action, emphazisedMode,
+ i % 2 != 0, p.ambient);
+ big.addView(R.id.actions, button);
+ }
+ } else {
+ big.setViewVisibility(R.id.actions_container, View.GONE);
+ }
+
+ CharSequence[] replyText = mN.extras.getCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY);
+ if (!p.ambient && validRemoteInput && replyText != null
+ && replyText.length > 0 && !TextUtils.isEmpty(replyText[0])) {
+ big.setViewVisibility(R.id.notification_material_reply_container, View.VISIBLE);
+ big.setTextViewText(R.id.notification_material_reply_text_1,
+ processTextSpans(replyText[0]));
+ setTextViewColorSecondary(big, R.id.notification_material_reply_text_1);
+
+ if (replyText.length > 1 && !TextUtils.isEmpty(replyText[1])) {
+ big.setViewVisibility(R.id.notification_material_reply_text_2, View.VISIBLE);
+ big.setTextViewText(R.id.notification_material_reply_text_2,
+ processTextSpans(replyText[1]));
+ setTextViewColorSecondary(big, R.id.notification_material_reply_text_2);
+
+ if (replyText.length > 2 && !TextUtils.isEmpty(replyText[2])) {
+ big.setViewVisibility(
+ R.id.notification_material_reply_text_3, View.VISIBLE);
+ big.setTextViewText(R.id.notification_material_reply_text_3,
+ processTextSpans(replyText[2]));
+ setTextViewColorSecondary(big, R.id.notification_material_reply_text_3);
+ }
+ }
+ }
+
+ return big;
+ }
+
+ private boolean hasValidRemoteInput(Action action) {
+ if (TextUtils.isEmpty(action.title) || action.actionIntent == null) {
+ // Weird actions
+ return false;
+ }
+
+ RemoteInput[] remoteInputs = action.getRemoteInputs();
+ if (remoteInputs == null) {
+ return false;
+ }
+
+ for (RemoteInput r : remoteInputs) {
+ CharSequence[] choices = r.getChoices();
+ if (r.getAllowFreeFormInput() || (choices != null && choices.length != 0)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Construct a RemoteViews for the final 1U notification layout. In order:
+ * 1. Custom contentView from the caller
+ * 2. Style's proposed content view
+ * 3. Standard template view
+ */
+ public RemoteViews createContentView() {
+ return createContentView(false /* increasedheight */ );
+ }
+
+ /**
+ * Construct a RemoteViews for the smaller content view.
+ *
+ * @param increasedHeight true if this layout be created with an increased height. Some
+ * styles may support showing more then just that basic 1U size
+ * and the system may decide to render important notifications
+ * slightly bigger even when collapsed.
+ *
+ * @hide
+ */
+ public RemoteViews createContentView(boolean increasedHeight) {
+ if (mN.contentView != null && useExistingRemoteView()) {
+ return mN.contentView;
+ } else if (mStyle != null) {
+ final RemoteViews styleView = mStyle.makeContentView(increasedHeight);
+ if (styleView != null) {
+ return styleView;
+ }
+ }
+ return applyStandardTemplate(getBaseLayoutResource());
+ }
+
+ private boolean useExistingRemoteView() {
+ return mStyle == null || (!mStyle.displayCustomViewInline()
+ && !mRebuildStyledRemoteViews);
+ }
+
+ /**
+ * Construct a RemoteViews for the final big notification layout.
+ */
+ public RemoteViews createBigContentView() {
+ RemoteViews result = null;
+ if (mN.bigContentView != null && useExistingRemoteView()) {
+ return mN.bigContentView;
+ } else if (mStyle != null) {
+ result = mStyle.makeBigContentView();
+ hideLine1Text(result);
+ } else if (mActions.size() != 0) {
+ result = applyStandardTemplateWithActions(getBigBaseLayoutResource());
+ }
+ makeHeaderExpanded(result);
+ return result;
+ }
+
+ /**
+ * Construct a RemoteViews for the final notification header only. This will not be
+ * colorized.
+ *
+ * @param ambient if true, generate the header for the ambient display layout.
+ * @hide
+ */
+ public RemoteViews makeNotificationHeader(boolean ambient) {
+ Boolean colorized = (Boolean) mN.extras.get(EXTRA_COLORIZED);
+ mN.extras.putBoolean(EXTRA_COLORIZED, false);
+ RemoteViews header = new BuilderRemoteViews(mContext.getApplicationInfo(),
+ ambient ? R.layout.notification_template_ambient_header
+ : R.layout.notification_template_header);
+ resetNotificationHeader(header);
+ bindNotificationHeader(header, ambient);
+ if (colorized != null) {
+ mN.extras.putBoolean(EXTRA_COLORIZED, colorized);
+ } else {
+ mN.extras.remove(EXTRA_COLORIZED);
+ }
+ return header;
+ }
+
+ /**
+ * Construct a RemoteViews for the ambient version of the notification.
+ *
+ * @hide
+ */
+ public RemoteViews makeAmbientNotification() {
+ RemoteViews ambient = applyStandardTemplateWithActions(
+ R.layout.notification_template_material_ambient,
+ mParams.reset().ambient(true).fillTextsFrom(this).hasProgress(false));
+ return ambient;
+ }
+
+ private void hideLine1Text(RemoteViews result) {
+ if (result != null) {
+ result.setViewVisibility(R.id.text_line_1, View.GONE);
+ }
+ }
+
+ /**
+ * Adapt the Notification header if this view is used as an expanded view.
+ *
+ * @hide
+ */
+ public static void makeHeaderExpanded(RemoteViews result) {
+ if (result != null) {
+ result.setBoolean(R.id.notification_header, "setExpanded", true);
+ }
+ }
+
+ /**
+ * Construct a RemoteViews for the final heads-up notification layout.
+ *
+ * @param increasedHeight true if this layout be created with an increased height. Some
+ * styles may support showing more then just that basic 1U size
+ * and the system may decide to render important notifications
+ * slightly bigger even when collapsed.
+ *
+ * @hide
+ */
+ public RemoteViews createHeadsUpContentView(boolean increasedHeight) {
+ if (mN.headsUpContentView != null && useExistingRemoteView()) {
+ return mN.headsUpContentView;
+ } else if (mStyle != null) {
+ final RemoteViews styleView = mStyle.makeHeadsUpContentView(increasedHeight);
+ if (styleView != null) {
+ return styleView;
+ }
+ } else if (mActions.size() == 0) {
+ return null;
+ }
+
+ return applyStandardTemplateWithActions(getBigBaseLayoutResource());
+ }
+
+ /**
+ * Construct a RemoteViews for the final heads-up notification layout.
+ */
+ public RemoteViews createHeadsUpContentView() {
+ return createHeadsUpContentView(false /* useIncreasedHeight */);
+ }
+
+ /**
+ * Construct a RemoteViews for the display in public contexts like on the lockscreen.
+ *
+ * @hide
+ */
+ public RemoteViews makePublicContentView() {
+ return makePublicView(false /* ambient */);
+ }
+
+ /**
+ * Construct a RemoteViews for the display in public contexts like on the lockscreen.
+ *
+ * @hide
+ */
+ public RemoteViews makePublicAmbientNotification() {
+ return makePublicView(true /* ambient */);
+ }
+
+ private RemoteViews makePublicView(boolean ambient) {
+ if (mN.publicVersion != null) {
+ final Builder builder = recoverBuilder(mContext, mN.publicVersion);
+ return ambient ? builder.makeAmbientNotification() : builder.createContentView();
+ }
+ Bundle savedBundle = mN.extras;
+ Style style = mStyle;
+ mStyle = null;
+ Icon largeIcon = mN.mLargeIcon;
+ mN.mLargeIcon = null;
+ Bitmap largeIconLegacy = mN.largeIcon;
+ mN.largeIcon = null;
+ ArrayList<Action> actions = mActions;
+ mActions = new ArrayList<>();
+ Bundle publicExtras = new Bundle();
+ publicExtras.putBoolean(EXTRA_SHOW_WHEN,
+ savedBundle.getBoolean(EXTRA_SHOW_WHEN));
+ publicExtras.putBoolean(EXTRA_SHOW_CHRONOMETER,
+ savedBundle.getBoolean(EXTRA_SHOW_CHRONOMETER));
+ publicExtras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN,
+ savedBundle.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN));
+ mN.extras = publicExtras;
+ RemoteViews view;
+ if (ambient) {
+ publicExtras.putCharSequence(EXTRA_TITLE,
+ mContext.getString(com.android.internal.R.string.notification_hidden_text));
+ view = makeAmbientNotification();
+ } else{
+ view = makeNotificationHeader(false /* ambient */);
+ view.setBoolean(R.id.notification_header, "setExpandOnlyOnButton", true);
+ }
+ mN.extras = savedBundle;
+ mN.mLargeIcon = largeIcon;
+ mN.largeIcon = largeIconLegacy;
+ mActions = actions;
+ mStyle = style;
+ return view;
+ }
+
+ /**
+ * Construct a content view for the display when low - priority
+ *
+ * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise
+ * a new subtext is created consisting of the content of the
+ * notification.
+ * @hide
+ */
+ public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) {
+ int color = mN.color;
+ mN.color = COLOR_DEFAULT;
+ CharSequence summary = mN.extras.getCharSequence(EXTRA_SUB_TEXT);
+ if (!useRegularSubtext || TextUtils.isEmpty(summary)) {
+ CharSequence newSummary = createSummaryText();
+ if (!TextUtils.isEmpty(newSummary)) {
+ mN.extras.putCharSequence(EXTRA_SUB_TEXT, newSummary);
+ }
+ }
+
+ RemoteViews header = makeNotificationHeader(false /* ambient */);
+ header.setBoolean(R.id.notification_header, "setAcceptAllTouches", true);
+ if (summary != null) {
+ mN.extras.putCharSequence(EXTRA_SUB_TEXT, summary);
+ } else {
+ mN.extras.remove(EXTRA_SUB_TEXT);
+ }
+ mN.color = color;
+ return header;
+ }
+
+ private CharSequence createSummaryText() {
+ CharSequence titleText = mN.extras.getCharSequence(Notification.EXTRA_TITLE);
+ if (USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY) {
+ return titleText;
+ }
+ SpannableStringBuilder summary = new SpannableStringBuilder();
+ if (titleText == null) {
+ titleText = mN.extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
+ }
+ BidiFormatter bidi = BidiFormatter.getInstance();
+ if (titleText != null) {
+ summary.append(bidi.unicodeWrap(titleText));
+ }
+ CharSequence contentText = mN.extras.getCharSequence(Notification.EXTRA_TEXT);
+ if (titleText != null && contentText != null) {
+ summary.append(bidi.unicodeWrap(mContext.getText(
+ R.string.notification_header_divider_symbol_with_spaces)));
+ }
+ if (contentText != null) {
+ summary.append(bidi.unicodeWrap(contentText));
+ }
+ return summary;
+ }
+
+ private RemoteViews generateActionButton(Action action, boolean emphazisedMode,
+ boolean oddAction, boolean ambient) {
+ final boolean tombstone = (action.actionIntent == null);
+ RemoteViews button = new BuilderRemoteViews(mContext.getApplicationInfo(),
+ emphazisedMode ? getEmphasizedActionLayoutResource()
+ : tombstone ? getActionTombstoneLayoutResource()
+ : getActionLayoutResource());
+ if (!tombstone) {
+ button.setOnClickPendingIntent(R.id.action0, action.actionIntent);
+ }
+ button.setContentDescription(R.id.action0, action.title);
+ if (action.mRemoteInputs != null) {
+ button.setRemoteInputs(R.id.action0, action.mRemoteInputs);
+ }
+ // TODO: handle emphasized mode / actions right
+ if (emphazisedMode) {
+ // change the background bgColor
+ int bgColor;
+ if (isColorized()) {
+ bgColor = oddAction ? getActionBarColor() : getActionBarColorDeEmphasized();
+ } else {
+ bgColor = mContext.getColor(oddAction ? R.color.notification_action_list
+ : R.color.notification_action_list_dark);
+ }
+ button.setDrawableParameters(R.id.button_holder, true, -1, bgColor,
+ PorterDuff.Mode.SRC_ATOP, -1);
+ CharSequence title = action.title;
+ ColorStateList[] outResultColor = null;
+ if (isLegacy()) {
+ title = NotificationColorUtil.clearColorSpans(title);
+ } else {
+ outResultColor = new ColorStateList[1];
+ title = ensureColorSpanContrast(title, bgColor, outResultColor);
+ }
+ button.setTextViewText(R.id.action0, processTextSpans(title));
+ setTextViewColorPrimary(button, R.id.action0);
+ if (outResultColor != null && outResultColor[0] != null) {
+ // We need to set the text color as well since changing a text to uppercase
+ // clears its spans.
+ button.setTextColor(R.id.action0, outResultColor[0]);
+ } else if (mN.color != COLOR_DEFAULT && !isColorized() && mTintActionButtons) {
+ button.setTextColor(R.id.action0,resolveContrastColor());
+ }
+ } else {
+ button.setTextViewText(R.id.action0, processTextSpans(
+ processLegacyText(action.title)));
+ if (isColorized() && !ambient) {
+ setTextViewColorPrimary(button, R.id.action0);
+ } else if (mN.color != COLOR_DEFAULT && mTintActionButtons) {
+ button.setTextColor(R.id.action0,
+ ambient ? resolveAmbientColor() : resolveContrastColor());
+ }
+ }
+ return button;
+ }
+
+ /**
+ * Ensures contrast on color spans against a background color. also returns the color of the
+ * text if a span was found that spans over the whole text.
+ *
+ * @param charSequence the charSequence on which the spans are
+ * @param background the background color to ensure the contrast against
+ * @param outResultColor an array in which a color will be returned as the first element if
+ * there exists a full length color span.
+ * @return the contrasted charSequence
+ */
+ private CharSequence ensureColorSpanContrast(CharSequence charSequence, int background,
+ ColorStateList[] outResultColor) {
+ if (charSequence instanceof Spanned) {
+ Spanned ss = (Spanned) charSequence;
+ Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+ SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+ for (Object span : spans) {
+ Object resultSpan = span;
+ int spanStart = ss.getSpanStart(span);
+ int spanEnd = ss.getSpanEnd(span);
+ boolean fullLength = (spanEnd - spanStart) == charSequence.length();
+ if (resultSpan instanceof CharacterStyle) {
+ resultSpan = ((CharacterStyle) span).getUnderlying();
+ }
+ if (resultSpan instanceof TextAppearanceSpan) {
+ TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
+ ColorStateList textColor = originalSpan.getTextColor();
+ if (textColor != null) {
+ int[] colors = textColor.getColors();
+ int[] newColors = new int[colors.length];
+ for (int i = 0; i < newColors.length; i++) {
+ newColors[i] = NotificationColorUtil.ensureLargeTextContrast(
+ colors[i], background, mInNightMode);
+ }
+ textColor = new ColorStateList(textColor.getStates().clone(),
+ newColors);
+ resultSpan = new TextAppearanceSpan(
+ originalSpan.getFamily(),
+ originalSpan.getTextStyle(),
+ originalSpan.getTextSize(),
+ textColor,
+ originalSpan.getLinkTextColor());
+ if (fullLength) {
+ outResultColor[0] = new ColorStateList(
+ textColor.getStates().clone(), newColors);
+ }
+ }
+ } else if (resultSpan instanceof ForegroundColorSpan) {
+ ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
+ int foregroundColor = originalSpan.getForegroundColor();
+ foregroundColor = NotificationColorUtil.ensureLargeTextContrast(
+ foregroundColor, background, mInNightMode);
+ resultSpan = new ForegroundColorSpan(foregroundColor);
+ if (fullLength) {
+ outResultColor[0] = ColorStateList.valueOf(foregroundColor);
+ }
+ } else {
+ resultSpan = span;
+ }
+
+ builder.setSpan(resultSpan, spanStart, spanEnd, ss.getSpanFlags(span));
+ }
+ return builder;
+ }
+ return charSequence;
+ }
+
+ /**
+ * @return Whether we are currently building a notification from a legacy (an app that
+ * doesn't create material notifications by itself) app.
+ */
+ private boolean isLegacy() {
+ if (!mIsLegacyInitialized) {
+ mIsLegacy = mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.LOLLIPOP;
+ mIsLegacyInitialized = true;
+ }
+ return mIsLegacy;
+ }
+
+ private CharSequence processLegacyText(CharSequence charSequence) {
+ return processLegacyText(charSequence, false /* ambient */);
+ }
+
+ private CharSequence processLegacyText(CharSequence charSequence, boolean ambient) {
+ boolean isAlreadyLightText = isLegacy() || textColorsNeedInversion();
+ boolean wantLightText = ambient;
+ if (isAlreadyLightText != wantLightText) {
+ return getColorUtil().invertCharSequenceColors(charSequence);
+ } else {
+ return charSequence;
+ }
+ }
+
+ /**
+ * Apply any necessariy colors to the small icon
+ */
+ private void processSmallIconColor(Icon smallIcon, RemoteViews contentView,
+ boolean ambient) {
+ boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, smallIcon);
+ int color = ambient ? resolveAmbientColor() : getPrimaryHighlightColor();
+ if (colorable) {
+ contentView.setDrawableParameters(R.id.icon, false, -1, color,
+ PorterDuff.Mode.SRC_ATOP, -1);
+
+ }
+ contentView.setInt(R.id.notification_header, "setOriginalIconColor",
+ colorable ? color : NotificationHeaderView.NO_COLOR);
+ }
+
+ /**
+ * Make the largeIcon dark if it's a fake smallIcon (that is,
+ * if it's grayscale).
+ */
+ // TODO: also check bounds, transparency, that sort of thing.
+ private void processLargeLegacyIcon(Icon largeIcon, RemoteViews contentView) {
+ if (largeIcon != null && isLegacy()
+ && getColorUtil().isGrayscaleIcon(mContext, largeIcon)) {
+ // resolve color will fall back to the default when legacy
+ contentView.setDrawableParameters(R.id.icon, false, -1, resolveContrastColor(),
+ PorterDuff.Mode.SRC_ATOP, -1);
+ }
+ }
+
+ private void sanitizeColor() {
+ if (mN.color != COLOR_DEFAULT) {
+ mN.color |= 0xFF000000; // no alpha for custom colors
+ }
+ }
+
+ int resolveContrastColor() {
+ if (mCachedContrastColorIsFor == mN.color && mCachedContrastColor != COLOR_INVALID) {
+ return mCachedContrastColor;
+ }
+
+ int color;
+ int background = mBackgroundColorHint;
+ if (mBackgroundColorHint == COLOR_INVALID) {
+ background = mContext.getColor(
+ com.android.internal.R.color.notification_material_background_color);
+ }
+ if (mN.color == COLOR_DEFAULT) {
+ ensureColors();
+ color = mSecondaryTextColor;
+ } else {
+ color = NotificationColorUtil.resolveContrastColor(mContext, mN.color,
+ background, mInNightMode);
+ }
+ if (Color.alpha(color) < 255) {
+ // alpha doesn't go well for color filters, so let's blend it manually
+ color = NotificationColorUtil.compositeColors(color, background);
+ }
+ mCachedContrastColorIsFor = mN.color;
+ return mCachedContrastColor = color;
+ }
+
+ int resolveAmbientColor() {
+ if (mCachedAmbientColorIsFor == mN.color && mCachedAmbientColorIsFor != COLOR_INVALID) {
+ return mCachedAmbientColor;
+ }
+ final int contrasted = NotificationColorUtil.resolveAmbientColor(mContext, mN.color);
+
+ mCachedAmbientColorIsFor = mN.color;
+ return mCachedAmbientColor = contrasted;
+ }
+
+ /**
+ * Apply the unstyled operations and return a new {@link Notification} object.
+ * @hide
+ */
+ public Notification buildUnstyled() {
+ if (mActions.size() > 0) {
+ mN.actions = new Action[mActions.size()];
+ mActions.toArray(mN.actions);
+ }
+ if (!mPersonList.isEmpty()) {
+ mN.extras.putStringArray(EXTRA_PEOPLE,
+ mPersonList.toArray(new String[mPersonList.size()]));
+ }
+ if (mN.bigContentView != null || mN.contentView != null
+ || mN.headsUpContentView != null) {
+ mN.extras.putBoolean(EXTRA_CONTAINS_CUSTOM_VIEW, true);
+ }
+ return mN;
+ }
+
+ /**
+ * Creates a Builder from an existing notification so further changes can be made.
+ * @param context The context for your application / activity.
+ * @param n The notification to create a Builder from.
+ */
+ public static Notification.Builder recoverBuilder(Context context, Notification n) {
+ // Re-create notification context so we can access app resources.
+ ApplicationInfo applicationInfo = n.extras.getParcelable(
+ EXTRA_BUILDER_APPLICATION_INFO);
+ Context builderContext;
+ if (applicationInfo != null) {
+ try {
+ builderContext = context.createApplicationContext(applicationInfo,
+ Context.CONTEXT_RESTRICTED);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "ApplicationInfo " + applicationInfo + " not found");
+ builderContext = context; // try with our context
+ }
+ } else {
+ builderContext = context; // try with given context
+ }
+
+ return new Builder(builderContext, n);
+ }
+
+ /**
+ * @deprecated Use {@link #build()} instead.
+ */
+ @Deprecated
+ public Notification getNotification() {
+ return build();
+ }
+
+ /**
+ * Combine all of the options that have been set and return a new {@link Notification}
+ * object.
+ */
+ public Notification build() {
+ // first, add any extras from the calling code
+ if (mUserExtras != null) {
+ mN.extras = getAllExtras();
+ }
+
+ mN.creationTime = System.currentTimeMillis();
+
+ // lazy stuff from mContext; see comment in Builder(Context, Notification)
+ Notification.addFieldsFromContext(mContext, mN);
+
+ buildUnstyled();
+
+ if (mStyle != null) {
+ mStyle.reduceImageSizes(mContext);
+ mStyle.purgeResources();
+ mStyle.buildStyled(mN);
+ }
+ mN.reduceImageSizes(mContext);
+
+ if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N
+ && (useExistingRemoteView())) {
+ if (mN.contentView == null) {
+ mN.contentView = createContentView();
+ mN.extras.putInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT,
+ mN.contentView.getSequenceNumber());
+ }
+ if (mN.bigContentView == null) {
+ mN.bigContentView = createBigContentView();
+ if (mN.bigContentView != null) {
+ mN.extras.putInt(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT,
+ mN.bigContentView.getSequenceNumber());
+ }
+ }
+ if (mN.headsUpContentView == null) {
+ mN.headsUpContentView = createHeadsUpContentView();
+ if (mN.headsUpContentView != null) {
+ mN.extras.putInt(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT,
+ mN.headsUpContentView.getSequenceNumber());
+ }
+ }
+ }
+
+ if ((mN.defaults & DEFAULT_LIGHTS) != 0) {
+ mN.flags |= FLAG_SHOW_LIGHTS;
+ }
+
+ return mN;
+ }
+
+ /**
+ * Apply this Builder to an existing {@link Notification} object.
+ *
+ * @hide
+ */
+ public Notification buildInto(Notification n) {
+ build().cloneInto(n, true);
+ return n;
+ }
+
+ /**
+ * Removes RemoteViews that were created for compatibility from {@param n}, if they did not
+ * change. Also removes extenders on low ram devices, as
+ * {@link android.service.notification.NotificationListenerService} services are disabled.
+ *
+ * @return {@param n}, if no stripping is needed, otherwise a stripped clone of {@param n}.
+ *
+ * @hide
+ */
+ public static Notification maybeCloneStrippedForDelivery(Notification n, boolean isLowRam) {
+ String templateClass = n.extras.getString(EXTRA_TEMPLATE);
+
+ // Only strip views for known Styles because we won't know how to
+ // re-create them otherwise.
+ if (!isLowRam
+ && !TextUtils.isEmpty(templateClass)
+ && getNotificationStyleClass(templateClass) == null) {
+ return n;
+ }
+
+ // Only strip unmodified BuilderRemoteViews.
+ boolean stripContentView = n.contentView instanceof BuilderRemoteViews &&
+ n.extras.getInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT, -1) ==
+ n.contentView.getSequenceNumber();
+ boolean stripBigContentView = n.bigContentView instanceof BuilderRemoteViews &&
+ n.extras.getInt(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT, -1) ==
+ n.bigContentView.getSequenceNumber();
+ boolean stripHeadsUpContentView = n.headsUpContentView instanceof BuilderRemoteViews &&
+ n.extras.getInt(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT, -1) ==
+ n.headsUpContentView.getSequenceNumber();
+
+ // Nothing to do here, no need to clone.
+ if (!isLowRam
+ && !stripContentView && !stripBigContentView && !stripHeadsUpContentView) {
+ return n;
+ }
+
+ Notification clone = n.clone();
+ if (stripContentView) {
+ clone.contentView = null;
+ clone.extras.remove(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT);
+ }
+ if (stripBigContentView) {
+ clone.bigContentView = null;
+ clone.extras.remove(EXTRA_REBUILD_BIG_CONTENT_VIEW_ACTION_COUNT);
+ }
+ if (stripHeadsUpContentView) {
+ clone.headsUpContentView = null;
+ clone.extras.remove(EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT);
+ }
+ if (isLowRam) {
+ clone.extras.remove(Notification.TvExtender.EXTRA_TV_EXTENDER);
+ clone.extras.remove(WearableExtender.EXTRA_WEARABLE_EXTENSIONS);
+ clone.extras.remove(CarExtender.EXTRA_CAR_EXTENDER);
+ }
+ return clone;
+ }
+
+ private int getBaseLayoutResource() {
+ return R.layout.notification_template_material_base;
+ }
+
+ private int getBigBaseLayoutResource() {
+ return R.layout.notification_template_material_big_base;
+ }
+
+ private int getBigPictureLayoutResource() {
+ return R.layout.notification_template_material_big_picture;
+ }
+
+ private int getBigTextLayoutResource() {
+ return R.layout.notification_template_material_big_text;
+ }
+
+ private int getInboxLayoutResource() {
+ return R.layout.notification_template_material_inbox;
+ }
+
+ private int getMessagingLayoutResource() {
+ return R.layout.notification_template_material_messaging;
+ }
+
+ private int getActionLayoutResource() {
+ return R.layout.notification_material_action;
+ }
+
+ private int getEmphasizedActionLayoutResource() {
+ return R.layout.notification_material_action_emphasized;
+ }
+
+ private int getActionTombstoneLayoutResource() {
+ return R.layout.notification_material_action_tombstone;
+ }
+
+ private int getBackgroundColor() {
+ if (isColorized()) {
+ return mBackgroundColor != COLOR_INVALID ? mBackgroundColor : mN.color;
+ } else {
+ return mBackgroundColorHint != COLOR_INVALID ? mBackgroundColorHint
+ : COLOR_DEFAULT;
+ }
+ }
+
+ private boolean isColorized() {
+ return mN.isColorized();
+ }
+
+ private boolean shouldTintActionButtons() {
+ return mTintActionButtons;
+ }
+
+ private boolean textColorsNeedInversion() {
+ if (mStyle == null || !MediaStyle.class.equals(mStyle.getClass())) {
+ return false;
+ }
+ int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+ return targetSdkVersion > Build.VERSION_CODES.M
+ && targetSdkVersion < Build.VERSION_CODES.O;
+ }
+
+ /**
+ * Set a color palette to be used as the background and textColors
+ *
+ * @param backgroundColor the color to be used as the background
+ * @param foregroundColor the color to be used as the foreground
+ *
+ * @hide
+ */
+ public void setColorPalette(int backgroundColor, int foregroundColor) {
+ mBackgroundColor = backgroundColor;
+ mForegroundColor = foregroundColor;
+ mTextColorsAreForBackground = COLOR_INVALID;
+ ensureColors();
+ }
+
+ /**
+ * Sets the background color for this notification to be a different one then the default.
+ * This is mainly used to calculate contrast and won't necessarily be applied to the
+ * background.
+ *
+ * @hide
+ */
+ public void setBackgroundColorHint(int backgroundColor) {
+ mBackgroundColorHint = backgroundColor;
+ }
+
+
+ /**
+ * Forces all styled remoteViews to be built from scratch and not use any cached
+ * RemoteViews.
+ * This is needed for legacy apps that are baking in their remoteviews into the
+ * notification.
+ *
+ * @hide
+ */
+ public void setRebuildStyledRemoteViews(boolean rebuild) {
+ mRebuildStyledRemoteViews = rebuild;
+ }
+ }
+
+ /**
+ * Reduces the image sizes to conform to a maximum allowed size. This also processes all custom
+ * remote views.
+ *
+ * @hide
+ */
+ void reduceImageSizes(Context context) {
+ if (extras.getBoolean(EXTRA_REDUCED_IMAGES)) {
+ return;
+ }
+ boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+ if (mLargeIcon != null || largeIcon != null) {
+ Resources resources = context.getResources();
+ Class<? extends Style> style = getNotificationStyle();
+ int maxWidth = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_right_icon_size_low_ram
+ : R.dimen.notification_right_icon_size);
+ int maxHeight = maxWidth;
+ if (MediaStyle.class.equals(style)
+ || DecoratedMediaCustomViewStyle.class.equals(style)) {
+ maxHeight = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_media_image_max_height_low_ram
+ : R.dimen.notification_media_image_max_height);
+ maxWidth = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_media_image_max_width_low_ram
+ : R.dimen.notification_media_image_max_width);
+ }
+ if (mLargeIcon != null) {
+ mLargeIcon.scaleDownIfNecessary(maxWidth, maxHeight);
+ }
+ if (largeIcon != null) {
+ largeIcon = Icon.scaleDownIfNecessary(largeIcon, maxWidth, maxHeight);
+ }
+ }
+ reduceImageSizesForRemoteView(contentView, context, isLowRam);
+ reduceImageSizesForRemoteView(headsUpContentView, context, isLowRam);
+ reduceImageSizesForRemoteView(bigContentView, context, isLowRam);
+ extras.putBoolean(EXTRA_REDUCED_IMAGES, true);
+ }
+
+ private void reduceImageSizesForRemoteView(RemoteViews remoteView, Context context,
+ boolean isLowRam) {
+ if (remoteView != null) {
+ Resources resources = context.getResources();
+ int maxWidth = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_custom_view_max_image_width_low_ram
+ : R.dimen.notification_custom_view_max_image_width);
+ int maxHeight = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_custom_view_max_image_height_low_ram
+ : R.dimen.notification_custom_view_max_image_height);
+ remoteView.reduceImageSizes(maxWidth, maxHeight);
+ }
+ }
+
+ /**
+ * @return whether this notification is a foreground service notification
+ */
+ private boolean isForegroundService() {
+ return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
+ }
+
+ /**
+ * @return whether this notification has a media session attached
+ * @hide
+ */
+ public boolean hasMediaSession() {
+ return extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) != null;
+ }
+
+ /**
+ * @return the style class of this notification
+ * @hide
+ */
+ public Class<? extends Notification.Style> getNotificationStyle() {
+ String templateClass = extras.getString(Notification.EXTRA_TEMPLATE);
+
+ if (!TextUtils.isEmpty(templateClass)) {
+ return Notification.getNotificationStyleClass(templateClass);
+ }
+ return null;
+ }
+
+ /**
+ * @return true if this notification is colorized.
+ *
+ * @hide
+ */
+ public boolean isColorized() {
+ if (isColorizedMedia()) {
+ return true;
+ }
+ return extras.getBoolean(EXTRA_COLORIZED)
+ && (hasColorizedPermission() || isForegroundService());
+ }
+
+ /**
+ * Returns whether an app can colorize due to the android.permission.USE_COLORIZED_NOTIFICATIONS
+ * permission. The permission is checked when a notification is enqueued.
+ */
+ private boolean hasColorizedPermission() {
+ return (flags & Notification.FLAG_CAN_COLORIZE) != 0;
+ }
+
+ /**
+ * @return true if this notification is colorized and it is a media notification
+ *
+ * @hide
+ */
+ public boolean isColorizedMedia() {
+ Class<? extends Style> style = getNotificationStyle();
+ if (MediaStyle.class.equals(style)) {
+ Boolean colorized = (Boolean) extras.get(EXTRA_COLORIZED);
+ if ((colorized == null || colorized) && hasMediaSession()) {
+ return true;
+ }
+ } else if (DecoratedMediaCustomViewStyle.class.equals(style)) {
+ if (extras.getBoolean(EXTRA_COLORIZED) && hasMediaSession()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * @return true if this is a media notification
+ *
+ * @hide
+ */
+ public boolean isMediaNotification() {
+ Class<? extends Style> style = getNotificationStyle();
+ if (MediaStyle.class.equals(style)) {
+ return true;
+ } else if (DecoratedMediaCustomViewStyle.class.equals(style)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean hasLargeIcon() {
+ return mLargeIcon != null || largeIcon != null;
+ }
+
+ /**
+ * @return true if the notification will show the time; false otherwise
+ * @hide
+ */
+ public boolean showsTime() {
+ return when != 0 && extras.getBoolean(EXTRA_SHOW_WHEN);
+ }
+
+ /**
+ * @return true if the notification will show a chronometer; false otherwise
+ * @hide
+ */
+ public boolean showsChronometer() {
+ return when != 0 && extras.getBoolean(EXTRA_SHOW_CHRONOMETER);
+ }
+
+ /**
+ * @removed
+ */
+ @SystemApi
+ public static Class<? extends Style> getNotificationStyleClass(String templateClass) {
+ Class<? extends Style>[] classes = new Class[] {
+ BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
+ DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
+ MessagingStyle.class };
+ for (Class<? extends Style> innerClass : classes) {
+ if (templateClass.equals(innerClass.getName())) {
+ return innerClass;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * An object that can apply a rich notification style to a {@link Notification.Builder}
+ * object.
+ */
+ public static abstract class Style {
+ private CharSequence mBigContentTitle;
+
+ /**
+ * @hide
+ */
+ protected CharSequence mSummaryText = null;
+
+ /**
+ * @hide
+ */
+ protected boolean mSummaryTextSet = false;
+
+ protected Builder mBuilder;
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ protected void internalSetBigContentTitle(CharSequence title) {
+ mBigContentTitle = title;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ protected void internalSetSummaryText(CharSequence cs) {
+ mSummaryText = cs;
+ mSummaryTextSet = true;
+ }
+
+ public void setBuilder(Builder builder) {
+ if (mBuilder != builder) {
+ mBuilder = builder;
+ if (mBuilder != null) {
+ mBuilder.setStyle(this);
+ }
+ }
+ }
+
+ protected void checkBuilder() {
+ if (mBuilder == null) {
+ throw new IllegalArgumentException("Style requires a valid Builder object");
+ }
+ }
+
+ protected RemoteViews getStandardView(int layoutId) {
+ checkBuilder();
+
+ // Nasty.
+ CharSequence oldBuilderContentTitle =
+ mBuilder.getAllExtras().getCharSequence(EXTRA_TITLE);
+ if (mBigContentTitle != null) {
+ mBuilder.setContentTitle(mBigContentTitle);
+ }
+
+ RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(layoutId);
+
+ mBuilder.getAllExtras().putCharSequence(EXTRA_TITLE, oldBuilderContentTitle);
+
+ if (mBigContentTitle != null && mBigContentTitle.equals("")) {
+ contentView.setViewVisibility(R.id.line1, View.GONE);
+ } else {
+ contentView.setViewVisibility(R.id.line1, View.VISIBLE);
+ }
+
+ return contentView;
+ }
+
+ /**
+ * Construct a Style-specific RemoteViews for the collapsed notification layout.
+ * The default implementation has nothing additional to add.
+ *
+ * @param increasedHeight true if this layout be created with an increased height.
+ * @hide
+ */
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ return null;
+ }
+
+ /**
+ * Construct a Style-specific RemoteViews for the final big notification layout.
+ * @hide
+ */
+ public RemoteViews makeBigContentView() {
+ return null;
+ }
+
+ /**
+ * Construct a Style-specific RemoteViews for the final HUN layout.
+ *
+ * @param increasedHeight true if this layout be created with an increased height.
+ * @hide
+ */
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ return null;
+ }
+
+ /**
+ * Apply any style-specific extras to this notification before shipping it out.
+ * @hide
+ */
+ public void addExtras(Bundle extras) {
+ if (mSummaryTextSet) {
+ extras.putCharSequence(EXTRA_SUMMARY_TEXT, mSummaryText);
+ }
+ if (mBigContentTitle != null) {
+ extras.putCharSequence(EXTRA_TITLE_BIG, mBigContentTitle);
+ }
+ extras.putString(EXTRA_TEMPLATE, this.getClass().getName());
+ }
+
+ /**
+ * Reconstruct the internal state of this Style object from extras.
+ * @hide
+ */
+ protected void restoreFromExtras(Bundle extras) {
+ if (extras.containsKey(EXTRA_SUMMARY_TEXT)) {
+ mSummaryText = extras.getCharSequence(EXTRA_SUMMARY_TEXT);
+ mSummaryTextSet = true;
+ }
+ if (extras.containsKey(EXTRA_TITLE_BIG)) {
+ mBigContentTitle = extras.getCharSequence(EXTRA_TITLE_BIG);
+ }
+ }
+
+
+ /**
+ * @hide
+ */
+ public Notification buildStyled(Notification wip) {
+ addExtras(wip.extras);
+ return wip;
+ }
+
+ /**
+ * @hide
+ */
+ public void purgeResources() {}
+
+ /**
+ * Calls {@link android.app.Notification.Builder#build()} on the Builder this Style is
+ * attached to.
+ *
+ * @return the fully constructed Notification.
+ */
+ public Notification build() {
+ checkBuilder();
+ return mBuilder.build();
+ }
+
+ /**
+ * @hide
+ * @return true if the style positions the progress bar on the second line; false if the
+ * style hides the progress bar
+ */
+ protected boolean hasProgress() {
+ return true;
+ }
+
+ /**
+ * @hide
+ * @return Whether we should put the summary be put into the notification header
+ */
+ public boolean hasSummaryInHeader() {
+ return true;
+ }
+
+ /**
+ * @hide
+ * @return Whether custom content views are displayed inline in the style
+ */
+ public boolean displayCustomViewInline() {
+ return false;
+ }
+
+ /**
+ * Reduces the image sizes contained in this style.
+ *
+ * @hide
+ */
+ public void reduceImageSizes(Context context) {
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include a large image attachment.
+ *
+ * Here's how you'd set the <code>BigPictureStyle</code> on a notification:
+ * <pre class="prettyprint">
+ * Notification notif = new Notification.Builder(mContext)
+ * .setContentTitle(&quot;New photo from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_post)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.BigPictureStyle()
+ * .bigPicture(aBigBitmap))
+ * .build();
+ * </pre>
+ *
+ * @see Notification#bigContentView
+ */
+ public static class BigPictureStyle extends Style {
+ private Bitmap mPicture;
+ private Icon mBigLargeIcon;
+ private boolean mBigLargeIconSet = false;
+
+ public BigPictureStyle() {
+ }
+
+ /**
+ * @deprecated use {@code BigPictureStyle()}.
+ */
+ @Deprecated
+ public BigPictureStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ public BigPictureStyle setBigContentTitle(CharSequence title) {
+ internalSetBigContentTitle(safeCharSequence(title));
+ return this;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ public BigPictureStyle setSummaryText(CharSequence cs) {
+ internalSetSummaryText(safeCharSequence(cs));
+ return this;
+ }
+
+ /**
+ * Provide the bitmap to be used as the payload for the BigPicture notification.
+ */
+ public BigPictureStyle bigPicture(Bitmap b) {
+ mPicture = b;
+ return this;
+ }
+
+ /**
+ * Override the large icon when the big notification is shown.
+ */
+ public BigPictureStyle bigLargeIcon(Bitmap b) {
+ return bigLargeIcon(b != null ? Icon.createWithBitmap(b) : null);
+ }
+
+ /**
+ * Override the large icon when the big notification is shown.
+ */
+ public BigPictureStyle bigLargeIcon(Icon icon) {
+ mBigLargeIconSet = true;
+ mBigLargeIcon = icon;
+ return this;
+ }
+
+ /** @hide */
+ public static final int MIN_ASHMEM_BITMAP_SIZE = 128 * (1 << 10);
+
+ /**
+ * @hide
+ */
+ @Override
+ public void purgeResources() {
+ super.purgeResources();
+ if (mPicture != null &&
+ mPicture.isMutable() &&
+ mPicture.getAllocationByteCount() >= MIN_ASHMEM_BITMAP_SIZE) {
+ mPicture = mPicture.createAshmemBitmap();
+ }
+ if (mBigLargeIcon != null) {
+ mBigLargeIcon.convertToAshmem();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void reduceImageSizes(Context context) {
+ super.reduceImageSizes(context);
+ Resources resources = context.getResources();
+ boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+ if (mPicture != null) {
+ int maxPictureWidth = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_big_picture_max_height_low_ram
+ : R.dimen.notification_big_picture_max_height);
+ int maxPictureHeight = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_big_picture_max_width_low_ram
+ : R.dimen.notification_big_picture_max_width);
+ mPicture = Icon.scaleDownIfNecessary(mPicture, maxPictureWidth, maxPictureHeight);
+ }
+ if (mBigLargeIcon != null) {
+ int rightIconSize = resources.getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_right_icon_size_low_ram
+ : R.dimen.notification_right_icon_size);
+ mBigLargeIcon.scaleDownIfNecessary(rightIconSize, rightIconSize);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public RemoteViews makeBigContentView() {
+ // Replace mN.mLargeIcon with mBigLargeIcon if mBigLargeIconSet
+ // This covers the following cases:
+ // 1. mBigLargeIconSet -> mBigLargeIcon (null or non-null) applies, overrides
+ // mN.mLargeIcon
+ // 2. !mBigLargeIconSet -> mN.mLargeIcon applies
+ Icon oldLargeIcon = null;
+ Bitmap largeIconLegacy = null;
+ if (mBigLargeIconSet) {
+ oldLargeIcon = mBuilder.mN.mLargeIcon;
+ mBuilder.mN.mLargeIcon = mBigLargeIcon;
+ // The legacy largeIcon might not allow us to clear the image, as it's taken in
+ // replacement if the other one is null. Because we're restoring these legacy icons
+ // for old listeners, this is in general non-null.
+ largeIconLegacy = mBuilder.mN.largeIcon;
+ mBuilder.mN.largeIcon = null;
+ }
+
+ RemoteViews contentView = getStandardView(mBuilder.getBigPictureLayoutResource());
+ if (mSummaryTextSet) {
+ contentView.setTextViewText(R.id.text, mBuilder.processTextSpans(
+ mBuilder.processLegacyText(mSummaryText)));
+ mBuilder.setTextViewColorSecondary(contentView, R.id.text);
+ contentView.setViewVisibility(R.id.text, View.VISIBLE);
+ }
+ mBuilder.setContentMinHeight(contentView, mBuilder.mN.hasLargeIcon());
+
+ if (mBigLargeIconSet) {
+ mBuilder.mN.mLargeIcon = oldLargeIcon;
+ mBuilder.mN.largeIcon = largeIconLegacy;
+ }
+
+ contentView.setImageViewBitmap(R.id.big_picture, mPicture);
+ return contentView;
+ }
+
+ /**
+ * @hide
+ */
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+
+ if (mBigLargeIconSet) {
+ extras.putParcelable(EXTRA_LARGE_ICON_BIG, mBigLargeIcon);
+ }
+ extras.putParcelable(EXTRA_PICTURE, mPicture);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+
+ if (extras.containsKey(EXTRA_LARGE_ICON_BIG)) {
+ mBigLargeIconSet = true;
+ mBigLargeIcon = extras.getParcelable(EXTRA_LARGE_ICON_BIG);
+ }
+ mPicture = extras.getParcelable(EXTRA_PICTURE);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean hasSummaryInHeader() {
+ return false;
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include a lot of text.
+ *
+ * Here's how you'd set the <code>BigTextStyle</code> on a notification:
+ * <pre class="prettyprint">
+ * Notification notif = new Notification.Builder(mContext)
+ * .setContentTitle(&quot;New mail from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.BigTextStyle()
+ * .bigText(aVeryLongString))
+ * .build();
+ * </pre>
+ *
+ * @see Notification#bigContentView
+ */
+ public static class BigTextStyle extends Style {
+
+ private CharSequence mBigText;
+
+ public BigTextStyle() {
+ }
+
+ /**
+ * @deprecated use {@code BigTextStyle()}.
+ */
+ @Deprecated
+ public BigTextStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ public BigTextStyle setBigContentTitle(CharSequence title) {
+ internalSetBigContentTitle(safeCharSequence(title));
+ return this;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ public BigTextStyle setSummaryText(CharSequence cs) {
+ internalSetSummaryText(safeCharSequence(cs));
+ return this;
+ }
+
+ /**
+ * Provide the longer text to be displayed in the big form of the
+ * template in place of the content text.
+ */
+ public BigTextStyle bigText(CharSequence cs) {
+ mBigText = safeCharSequence(cs);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+
+ extras.putCharSequence(EXTRA_BIG_TEXT, mBigText);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+
+ mBigText = extras.getCharSequence(EXTRA_BIG_TEXT);
+ }
+
+ /**
+ * @param increasedHeight true if this layout be created with an increased height.
+ *
+ * @hide
+ */
+ @Override
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ if (increasedHeight) {
+ mBuilder.mOriginalActions = mBuilder.mActions;
+ mBuilder.mActions = new ArrayList<>();
+ RemoteViews remoteViews = makeBigContentView();
+ mBuilder.mActions = mBuilder.mOriginalActions;
+ mBuilder.mOriginalActions = null;
+ return remoteViews;
+ }
+ return super.makeContentView(increasedHeight);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ if (increasedHeight && mBuilder.mActions.size() > 0) {
+ return makeBigContentView();
+ }
+ return super.makeHeadsUpContentView(increasedHeight);
+ }
+
+ /**
+ * @hide
+ */
+ public RemoteViews makeBigContentView() {
+
+ // Nasty
+ CharSequence text = mBuilder.getAllExtras().getCharSequence(EXTRA_TEXT);
+ mBuilder.getAllExtras().putCharSequence(EXTRA_TEXT, null);
+
+ RemoteViews contentView = getStandardView(mBuilder.getBigTextLayoutResource());
+
+ mBuilder.getAllExtras().putCharSequence(EXTRA_TEXT, text);
+
+ CharSequence bigTextText = mBuilder.processLegacyText(mBigText);
+ if (TextUtils.isEmpty(bigTextText)) {
+ // In case the bigtext is null / empty fall back to the normal text to avoid a weird
+ // experience
+ bigTextText = mBuilder.processLegacyText(text);
+ }
+ applyBigTextContentView(mBuilder, contentView, bigTextText);
+
+ return contentView;
+ }
+
+ static void applyBigTextContentView(Builder builder,
+ RemoteViews contentView, CharSequence bigTextText) {
+ contentView.setTextViewText(R.id.big_text, builder.processTextSpans(bigTextText));
+ builder.setTextViewColorSecondary(contentView, R.id.big_text);
+ contentView.setViewVisibility(R.id.big_text,
+ TextUtils.isEmpty(bigTextText) ? View.GONE : View.VISIBLE);
+ contentView.setBoolean(R.id.big_text, "setHasImage", builder.mN.hasLargeIcon());
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include multiple back-and-forth
+ * messages of varying types between any number of people.
+ *
+ * <br>
+ * If the platform does not provide large-format notifications, this method has no effect. The
+ * user will always see the normal notification view.
+ * <br>
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like
+ * so:
+ * <pre class="prettyprint">
+ *
+ * Notification noti = new Notification.Builder()
+ * .setContentTitle(&quot;2 new messages wtih &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_message)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.MessagingStyle(resources.getString(R.string.reply_name))
+ * .addMessage(messages[0].getText(), messages[0].getTime(), messages[0].getSender())
+ * .addMessage(messages[1].getText(), messages[1].getTime(), messages[1].getSender()))
+ * .build();
+ * </pre>
+ */
+ public static class MessagingStyle extends Style {
+
+ /**
+ * The maximum number of messages that will be retained in the Notification itself (the
+ * number displayed is up to the platform).
+ */
+ public static final int MAXIMUM_RETAINED_MESSAGES = 25;
+
+ CharSequence mUserDisplayName;
+ CharSequence mConversationTitle;
+ List<Message> mMessages = new ArrayList<>();
+ List<Message> mHistoricMessages = new ArrayList<>();
+
+ MessagingStyle() {
+ }
+
+ /**
+ * @param userDisplayName Required - the name to be displayed for any replies sent by the
+ * user before the posting app reposts the notification with those messages after they've
+ * been actually sent and in previous messages sent by the user added in
+ * {@link #addMessage(Notification.MessagingStyle.Message)}
+ */
+ public MessagingStyle(@NonNull CharSequence userDisplayName) {
+ mUserDisplayName = userDisplayName;
+ }
+
+ /**
+ * Returns the name to be displayed for any replies sent by the user
+ */
+ public CharSequence getUserDisplayName() {
+ return mUserDisplayName;
+ }
+
+ /**
+ * Sets the title to be displayed on this conversation. This should only be used for
+ * group messaging and left unset for one-on-one conversations.
+ * @param conversationTitle
+ * @return this object for method chaining.
+ */
+ public MessagingStyle setConversationTitle(CharSequence conversationTitle) {
+ mConversationTitle = conversationTitle;
+ return this;
+ }
+
+ /**
+ * Return the title to be displayed on this conversation. Can be <code>null</code> and
+ * should be for one-on-one conversations
+ */
+ public CharSequence getConversationTitle() {
+ return mConversationTitle;
+ }
+
+ /**
+ * Adds a message for display by this notification. Convenience call for a simple
+ * {@link Message} in {@link #addMessage(Notification.MessagingStyle.Message)}.
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender A {@link CharSequence} to be used for displaying the name of the
+ * sender. Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert {@link #getUserDisplayName()}.
+ * Should be unique amongst all individuals in the conversation, and should be
+ * consistent during re-posts of the notification.
+ *
+ * @see Message#Message(CharSequence, long, CharSequence)
+ *
+ * @return this object for method chaining
+ */
+ public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
+ return addMessage(new Message(text, timestamp, sender));
+ }
+
+ /**
+ * Adds a {@link Message} for display in this notification.
+ *
+ * <p>The messages should be added in chronologic order, i.e. the oldest first,
+ * the newest last.
+ *
+ * @param message The {@link Message} to be displayed
+ * @return this object for method chaining
+ */
+ public MessagingStyle addMessage(Message message) {
+ mMessages.add(message);
+ if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
+ mMessages.remove(0);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a {@link Message} for historic context in this notification.
+ *
+ * <p>Messages should be added as historic if they are not the main subject of the
+ * notification but may give context to a conversation. The system may choose to present
+ * them only when relevant, e.g. when replying to a message through a {@link RemoteInput}.
+ *
+ * <p>The messages should be added in chronologic order, i.e. the oldest first,
+ * the newest last.
+ *
+ * @param message The historic {@link Message} to be added
+ * @return this object for method chaining
+ */
+ public MessagingStyle addHistoricMessage(Message message) {
+ mHistoricMessages.add(message);
+ if (mHistoricMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
+ mHistoricMessages.remove(0);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the list of {@code Message} objects that represent the notification
+ */
+ public List<Message> getMessages() {
+ return mMessages;
+ }
+
+ /**
+ * Gets the list of historic {@code Message}s in the notification.
+ */
+ public List<Message> getHistoricMessages() {
+ return mHistoricMessages;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+ if (mUserDisplayName != null) {
+ extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUserDisplayName);
+ }
+ if (mConversationTitle != null) {
+ extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
+ }
+ if (!mMessages.isEmpty()) { extras.putParcelableArray(EXTRA_MESSAGES,
+ Message.getBundleArrayForMessages(mMessages));
+ }
+ if (!mHistoricMessages.isEmpty()) { extras.putParcelableArray(EXTRA_HISTORIC_MESSAGES,
+ Message.getBundleArrayForMessages(mHistoricMessages));
+ }
+
+ fixTitleAndTextExtras(extras);
+ }
+
+ private void fixTitleAndTextExtras(Bundle extras) {
+ Message m = findLatestIncomingMessage();
+ CharSequence text = (m == null) ? null : m.mText;
+ CharSequence sender = m == null ? null
+ : TextUtils.isEmpty(m.mSender) ? mUserDisplayName : m.mSender;
+ CharSequence title;
+ if (!TextUtils.isEmpty(mConversationTitle)) {
+ if (!TextUtils.isEmpty(sender)) {
+ BidiFormatter bidi = BidiFormatter.getInstance();
+ title = mBuilder.mContext.getString(
+ com.android.internal.R.string.notification_messaging_title_template,
+ bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(m.mSender));
+ } else {
+ title = mConversationTitle;
+ }
+ } else {
+ title = sender;
+ }
+
+ if (title != null) {
+ extras.putCharSequence(EXTRA_TITLE, title);
+ }
+ if (text != null) {
+ extras.putCharSequence(EXTRA_TEXT, text);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+
+ mMessages.clear();
+ mHistoricMessages.clear();
+ mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
+ mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
+ Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
+ if (messages != null && messages instanceof Parcelable[]) {
+ mMessages = Message.getMessagesFromBundleArray(messages);
+ }
+ Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES);
+ if (histMessages != null && histMessages instanceof Parcelable[]) {
+ mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ if (!increasedHeight) {
+ Message m = findLatestIncomingMessage();
+ CharSequence title = mConversationTitle != null
+ ? mConversationTitle
+ : (m == null) ? null : m.mSender;
+ CharSequence text = (m == null)
+ ? null
+ : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText;
+
+ return mBuilder.applyStandardTemplate(mBuilder.getBaseLayoutResource(),
+ mBuilder.mParams.reset().hasProgress(false).title(title).text(text));
+ } else {
+ mBuilder.mOriginalActions = mBuilder.mActions;
+ mBuilder.mActions = new ArrayList<>();
+ RemoteViews remoteViews = makeBigContentView();
+ mBuilder.mActions = mBuilder.mOriginalActions;
+ mBuilder.mOriginalActions = null;
+ return remoteViews;
+ }
+ }
+
+ private Message findLatestIncomingMessage() {
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ Message m = mMessages.get(i);
+ // Incoming messages have a non-empty sender.
+ if (!TextUtils.isEmpty(m.mSender)) {
+ return m;
+ }
+ }
+ if (!mMessages.isEmpty()) {
+ // No incoming messages, fall back to outgoing message
+ return mMessages.get(mMessages.size() - 1);
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeBigContentView() {
+ CharSequence title = !TextUtils.isEmpty(super.mBigContentTitle)
+ ? super.mBigContentTitle
+ : mConversationTitle;
+ boolean hasTitle = !TextUtils.isEmpty(title);
+
+ if (mMessages.size() == 1) {
+ // Special case for a single message: Use the big text style
+ // so the collapsed and expanded versions match nicely.
+ CharSequence bigTitle;
+ CharSequence text;
+ if (hasTitle) {
+ bigTitle = title;
+ text = makeMessageLine(mMessages.get(0), mBuilder);
+ } else {
+ bigTitle = mMessages.get(0).mSender;
+ text = mMessages.get(0).mText;
+ }
+ RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
+ mBuilder.getBigTextLayoutResource(),
+ mBuilder.mParams.reset().hasProgress(false).title(bigTitle).text(null));
+ BigTextStyle.applyBigTextContentView(mBuilder, contentView, text);
+ return contentView;
+ }
+
+ RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
+ mBuilder.getMessagingLayoutResource(),
+ mBuilder.mParams.reset().hasProgress(false).title(title).text(null));
+
+ int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3,
+ R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6};
+
+ // Make sure all rows are gone in case we reuse a view.
+ for (int rowId : rowIds) {
+ contentView.setViewVisibility(rowId, View.GONE);
+ }
+
+ int i=0;
+ contentView.setViewLayoutMarginBottomDimen(R.id.line1,
+ hasTitle ? R.dimen.notification_messaging_spacing : 0);
+ contentView.setInt(R.id.notification_messaging, "setNumIndentLines",
+ !mBuilder.mN.hasLargeIcon() ? 0 : (hasTitle ? 1 : 2));
+
+ int contractedChildId = View.NO_ID;
+ Message contractedMessage = findLatestIncomingMessage();
+ int firstHistoricMessage = Math.max(0, mHistoricMessages.size()
+ - (rowIds.length - mMessages.size()));
+ while (firstHistoricMessage + i < mHistoricMessages.size() && i < rowIds.length) {
+ Message m = mHistoricMessages.get(firstHistoricMessage + i);
+ int rowId = rowIds[i];
+
+ contentView.setTextViewText(rowId, makeMessageLine(m, mBuilder));
+
+ if (contractedMessage == m) {
+ contractedChildId = rowId;
+ }
+
+ i++;
+ }
+
+ int firstMessage = Math.max(0, mMessages.size() - rowIds.length);
+ while (firstMessage + i < mMessages.size() && i < rowIds.length) {
+ Message m = mMessages.get(firstMessage + i);
+ int rowId = rowIds[i];
+
+ contentView.setViewVisibility(rowId, View.VISIBLE);
+ contentView.setTextViewText(rowId, mBuilder.processTextSpans(
+ makeMessageLine(m, mBuilder)));
+ mBuilder.setTextViewColorSecondary(contentView, rowId);
+
+ if (contractedMessage == m) {
+ contractedChildId = rowId;
+ }
+
+ i++;
+ }
+ // Clear the remaining views for reapply. Ensures that historic message views can
+ // reliably be identified as being GONE and having non-null text.
+ while (i < rowIds.length) {
+ int rowId = rowIds[i];
+ contentView.setTextViewText(rowId, null);
+ i++;
+ }
+
+ // Record this here to allow transformation between the contracted and expanded views.
+ contentView.setInt(R.id.notification_messaging, "setContractedChildId",
+ contractedChildId);
+ return contentView;
+ }
+
+ private CharSequence makeMessageLine(Message m, Builder builder) {
+ BidiFormatter bidi = BidiFormatter.getInstance();
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ boolean colorize = builder.isColorized();
+ TextAppearanceSpan colorSpan;
+ CharSequence messageName;
+ if (TextUtils.isEmpty(m.mSender)) {
+ CharSequence replyName = mUserDisplayName == null ? "" : mUserDisplayName;
+ sb.append(bidi.unicodeWrap(replyName),
+ makeFontColorSpan(colorize
+ ? builder.getPrimaryTextColor()
+ : mBuilder.resolveContrastColor()),
+ 0 /* flags */);
+ } else {
+ sb.append(bidi.unicodeWrap(m.mSender),
+ makeFontColorSpan(colorize
+ ? builder.getPrimaryTextColor()
+ : Color.BLACK),
+ 0 /* flags */);
+ }
+ CharSequence text = m.mText == null ? "" : m.mText;
+ sb.append(" ").append(bidi.unicodeWrap(text));
+ return sb;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ if (increasedHeight) {
+ return makeBigContentView();
+ }
+ Message m = findLatestIncomingMessage();
+ CharSequence title = mConversationTitle != null
+ ? mConversationTitle
+ : (m == null) ? null : m.mSender;
+ CharSequence text = (m == null)
+ ? null
+ : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText;
+
+ return mBuilder.applyStandardTemplateWithActions(mBuilder.getBigBaseLayoutResource(),
+ mBuilder.mParams.reset().hasProgress(false).title(title).text(text));
+ }
+
+ private static TextAppearanceSpan makeFontColorSpan(int color) {
+ return new TextAppearanceSpan(null, 0, 0,
+ ColorStateList.valueOf(color), null);
+ }
+
+ public static final class Message {
+
+ static final String KEY_TEXT = "text";
+ static final String KEY_TIMESTAMP = "time";
+ static final String KEY_SENDER = "sender";
+ static final String KEY_DATA_MIME_TYPE = "type";
+ static final String KEY_DATA_URI= "uri";
+ static final String KEY_EXTRAS_BUNDLE = "extras";
+
+ private final CharSequence mText;
+ private final long mTimestamp;
+ private final CharSequence mSender;
+
+ private Bundle mExtras = new Bundle();
+ private String mDataMimeType;
+ private Uri mDataUri;
+
+ /**
+ * Constructor
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender A {@link CharSequence} to be used for displaying the name of the
+ * sender. Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert {@link MessagingStyle#getUserDisplayName()}.
+ * Should be unique amongst all individuals in the conversation, and should be
+ * consistent during re-posts of the notification.
+ */
+ public Message(CharSequence text, long timestamp, CharSequence sender){
+ mText = text;
+ mTimestamp = timestamp;
+ mSender = sender;
+ }
+
+ /**
+ * Sets a binary blob of data and an associated MIME type for a message. In the case
+ * where the platform doesn't support the MIME type, the original text provided in the
+ * constructor will be used.
+ * @param dataMimeType The MIME type of the content. See
+ * <a href="{@docRoot}notifications/messaging.html"> for the list of supported MIME
+ * types on Android and Android Wear.
+ * @param dataUri The uri containing the content whose type is given by the MIME type.
+ * <p class="note">
+ * <ol>
+ * <li>Notification Listeners including the System UI need permission to access the
+ * data the Uri points to. The recommended ways to do this are:</li>
+ * <li>Store the data in your own ContentProvider, making sure that other apps have
+ * the correct permission to access your provider. The preferred mechanism for
+ * providing access is to use per-URI permissions which are temporary and only
+ * grant access to the receiving application. An easy way to create a
+ * ContentProvider like this is to use the FileProvider helper class.</li>
+ * <li>Use the system MediaStore. The MediaStore is primarily aimed at video, audio
+ * and image MIME types, however beginning with Android 3.0 (API level 11) it can
+ * also store non-media types (see MediaStore.Files for more info). Files can be
+ * inserted into the MediaStore using scanFile() after which a content:// style
+ * Uri suitable for sharing is passed to the provided onScanCompleted() callback.
+ * Note that once added to the system MediaStore the content is accessible to any
+ * app on the device.</li>
+ * </ol>
+ * @return this object for method chaining
+ */
+ public Message setData(String dataMimeType, Uri dataUri) {
+ mDataMimeType = dataMimeType;
+ mDataUri = dataUri;
+ return this;
+ }
+
+ /**
+ * Get the text to be used for this message, or the fallback text if a type and content
+ * Uri have been set
+ */
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Get the time at which this message arrived
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * Get the extras Bundle for this message.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Get the text used to display the contact's name in the messaging experience
+ */
+ public CharSequence getSender() {
+ return mSender;
+ }
+
+ /**
+ * Get the MIME type of the data pointed to by the Uri
+ */
+ public String getDataMimeType() {
+ return mDataMimeType;
+ }
+
+ /**
+ * Get the the Uri pointing to the content of the message. Can be null, in which case
+ * {@see #getText()} is used.
+ */
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ private Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ if (mText != null) {
+ bundle.putCharSequence(KEY_TEXT, mText);
+ }
+ bundle.putLong(KEY_TIMESTAMP, mTimestamp);
+ if (mSender != null) {
+ bundle.putCharSequence(KEY_SENDER, mSender);
+ }
+ if (mDataMimeType != null) {
+ bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
+ }
+ if (mDataUri != null) {
+ bundle.putParcelable(KEY_DATA_URI, mDataUri);
+ }
+ if (mExtras != null) {
+ bundle.putBundle(KEY_EXTRAS_BUNDLE, mExtras);
+ }
+ return bundle;
+ }
+
+ static Bundle[] getBundleArrayForMessages(List<Message> messages) {
+ Bundle[] bundles = new Bundle[messages.size()];
+ final int N = messages.size();
+ for (int i = 0; i < N; i++) {
+ bundles[i] = messages.get(i).toBundle();
+ }
+ return bundles;
+ }
+
+ static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
+ List<Message> messages = new ArrayList<>(bundles.length);
+ for (int i = 0; i < bundles.length; i++) {
+ if (bundles[i] instanceof Bundle) {
+ Message message = getMessageFromBundle((Bundle)bundles[i]);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ }
+ return messages;
+ }
+
+ static Message getMessageFromBundle(Bundle bundle) {
+ try {
+ if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
+ return null;
+ } else {
+ Message message = new Message(bundle.getCharSequence(KEY_TEXT),
+ bundle.getLong(KEY_TIMESTAMP), bundle.getCharSequence(KEY_SENDER));
+ if (bundle.containsKey(KEY_DATA_MIME_TYPE) &&
+ bundle.containsKey(KEY_DATA_URI)) {
+ message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
+ (Uri) bundle.getParcelable(KEY_DATA_URI));
+ }
+ if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) {
+ message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE));
+ }
+ return message;
+ }
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper class for generating large-format notifications that include a list of (up to 5) strings.
+ *
+ * Here's how you'd set the <code>InboxStyle</code> on a notification:
+ * <pre class="prettyprint">
+ * Notification notif = new Notification.Builder(mContext)
+ * .setContentTitle(&quot;5 New mails from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .setLargeIcon(aBitmap)
+ * .setStyle(new Notification.InboxStyle()
+ * .addLine(str1)
+ * .addLine(str2)
+ * .setContentTitle(&quot;&quot;)
+ * .setSummaryText(&quot;+3 more&quot;))
+ * .build();
+ * </pre>
+ *
+ * @see Notification#bigContentView
+ */
+ public static class InboxStyle extends Style {
+ private ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(5);
+
+ public InboxStyle() {
+ }
+
+ /**
+ * @deprecated use {@code InboxStyle()}.
+ */
+ @Deprecated
+ public InboxStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Overrides ContentTitle in the big form of the template.
+ * This defaults to the value passed to setContentTitle().
+ */
+ public InboxStyle setBigContentTitle(CharSequence title) {
+ internalSetBigContentTitle(safeCharSequence(title));
+ return this;
+ }
+
+ /**
+ * Set the first line of text after the detail section in the big form of the template.
+ */
+ public InboxStyle setSummaryText(CharSequence cs) {
+ internalSetSummaryText(safeCharSequence(cs));
+ return this;
+ }
+
+ /**
+ * Append a line to the digest section of the Inbox notification.
+ */
+ public InboxStyle addLine(CharSequence cs) {
+ mTexts.add(safeCharSequence(cs));
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+
+ CharSequence[] a = new CharSequence[mTexts.size()];
+ extras.putCharSequenceArray(EXTRA_TEXT_LINES, mTexts.toArray(a));
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+
+ mTexts.clear();
+ if (extras.containsKey(EXTRA_TEXT_LINES)) {
+ Collections.addAll(mTexts, extras.getCharSequenceArray(EXTRA_TEXT_LINES));
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public RemoteViews makeBigContentView() {
+ // Remove the content text so it disappears unless you have a summary
+ // Nasty
+ CharSequence oldBuilderContentText = mBuilder.mN.extras.getCharSequence(EXTRA_TEXT);
+ mBuilder.getAllExtras().putCharSequence(EXTRA_TEXT, null);
+
+ RemoteViews contentView = getStandardView(mBuilder.getInboxLayoutResource());
+
+ mBuilder.getAllExtras().putCharSequence(EXTRA_TEXT, oldBuilderContentText);
+
+ int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3,
+ R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6};
+
+ // Make sure all rows are gone in case we reuse a view.
+ for (int rowId : rowIds) {
+ contentView.setViewVisibility(rowId, View.GONE);
+ }
+
+ int i=0;
+ int topPadding = mBuilder.mContext.getResources().getDimensionPixelSize(
+ R.dimen.notification_inbox_item_top_padding);
+ boolean first = true;
+ int onlyViewId = 0;
+ int maxRows = rowIds.length;
+ if (mBuilder.mActions.size() > 0) {
+ maxRows--;
+ }
+ while (i < mTexts.size() && i < maxRows) {
+ CharSequence str = mTexts.get(i);
+ if (!TextUtils.isEmpty(str)) {
+ contentView.setViewVisibility(rowIds[i], View.VISIBLE);
+ contentView.setTextViewText(rowIds[i],
+ mBuilder.processTextSpans(mBuilder.processLegacyText(str)));
+ mBuilder.setTextViewColorSecondary(contentView, rowIds[i]);
+ contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0);
+ handleInboxImageMargin(contentView, rowIds[i], first);
+ if (first) {
+ onlyViewId = rowIds[i];
+ } else {
+ onlyViewId = 0;
+ }
+ first = false;
+ }
+ i++;
+ }
+ if (onlyViewId != 0) {
+ // We only have 1 entry, lets make it look like the normal Text of a Bigtext
+ topPadding = mBuilder.mContext.getResources().getDimensionPixelSize(
+ R.dimen.notification_text_margin_top);
+ contentView.setViewPadding(onlyViewId, 0, topPadding, 0, 0);
+ }
+
+ return contentView;
+ }
+
+ private void handleInboxImageMargin(RemoteViews contentView, int id, boolean first) {
+ int endMargin = 0;
+ if (first) {
+ final int max = mBuilder.mN.extras.getInt(EXTRA_PROGRESS_MAX, 0);
+ final boolean ind = mBuilder.mN.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE);
+ boolean hasProgress = max != 0 || ind;
+ if (mBuilder.mN.hasLargeIcon() && !hasProgress) {
+ endMargin = R.dimen.notification_content_picture_margin;
+ }
+ }
+ contentView.setViewLayoutMarginEndDimen(id, endMargin);
+ }
+ }
+
+ /**
+ * Notification style for media playback notifications.
+ *
+ * In the expanded form, {@link Notification#bigContentView}, up to 5
+ * {@link Notification.Action}s specified with
+ * {@link Notification.Builder#addAction(Action) addAction} will be
+ * shown as icon-only pushbuttons, suitable for transport controls. The Bitmap given to
+ * {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap) setLargeIcon()} will be
+ * treated as album artwork.
+ * <p>
+ * Unlike the other styles provided here, MediaStyle can also modify the standard-size
+ * {@link Notification#contentView}; by providing action indices to
+ * {@link #setShowActionsInCompactView(int...)} you can promote up to 3 actions to be displayed
+ * in the standard view alongside the usual content.
+ * <p>
+ * Notifications created with MediaStyle will have their category set to
+ * {@link Notification#CATEGORY_TRANSPORT CATEGORY_TRANSPORT} unless you set a different
+ * category using {@link Notification.Builder#setCategory(String) setCategory()}.
+ * <p>
+ * Finally, if you attach a {@link android.media.session.MediaSession.Token} using
+ * {@link android.app.Notification.MediaStyle#setMediaSession(MediaSession.Token)},
+ * the System UI can identify this as a notification representing an active media session
+ * and respond accordingly (by showing album artwork in the lockscreen, for example).
+ *
+ * <p>
+ * Starting at {@link android.os.Build.VERSION_CODES#O Android O} any notification that has a
+ * media session attached with {@link #setMediaSession(MediaSession.Token)} will be colorized.
+ * You can opt-out of this behavior by using {@link Notification.Builder#setColorized(boolean)}.
+ * <p>
+ *
+ * To use this style with your Notification, feed it to
+ * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.Builder()
+ * .setSmallIcon(R.drawable.ic_stat_player)
+ * .setContentTitle(&quot;Track title&quot;)
+ * .setContentText(&quot;Artist - Album&quot;)
+ * .setLargeIcon(albumArtBitmap))
+ * .setStyle(<b>new Notification.MediaStyle()</b>
+ * .setMediaSession(mySession))
+ * .build();
+ * </pre>
+ *
+ * @see Notification#bigContentView
+ * @see Notification.Builder#setColorized(boolean)
+ */
+ public static class MediaStyle extends Style {
+ static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3;
+ static final int MAX_MEDIA_BUTTONS = 5;
+
+ private int[] mActionsToShowInCompact = null;
+ private MediaSession.Token mToken;
+
+ public MediaStyle() {
+ }
+
+ /**
+ * @deprecated use {@code MediaStyle()}.
+ */
+ @Deprecated
+ public MediaStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ /**
+ * Request up to 3 actions (by index in the order of addition) to be shown in the compact
+ * notification view.
+ *
+ * @param actions the indices of the actions to show in the compact notification view
+ */
+ public MediaStyle setShowActionsInCompactView(int...actions) {
+ mActionsToShowInCompact = actions;
+ return this;
+ }
+
+ /**
+ * Attach a {@link android.media.session.MediaSession.Token} to this Notification
+ * to provide additional playback information and control to the SystemUI.
+ */
+ public MediaStyle setMediaSession(MediaSession.Token token) {
+ mToken = token;
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public Notification buildStyled(Notification wip) {
+ super.buildStyled(wip);
+ if (wip.category == null) {
+ wip.category = Notification.CATEGORY_TRANSPORT;
+ }
+ return wip;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ return makeMediaContentView();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeBigContentView() {
+ return makeMediaBigContentView();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ RemoteViews expanded = makeMediaBigContentView();
+ return expanded != null ? expanded : makeMediaContentView();
+ }
+
+ /** @hide */
+ @Override
+ public void addExtras(Bundle extras) {
+ super.addExtras(extras);
+
+ if (mToken != null) {
+ extras.putParcelable(EXTRA_MEDIA_SESSION, mToken);
+ }
+ if (mActionsToShowInCompact != null) {
+ extras.putIntArray(EXTRA_COMPACT_ACTIONS, mActionsToShowInCompact);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void restoreFromExtras(Bundle extras) {
+ super.restoreFromExtras(extras);
+
+ if (extras.containsKey(EXTRA_MEDIA_SESSION)) {
+ mToken = extras.getParcelable(EXTRA_MEDIA_SESSION);
+ }
+ if (extras.containsKey(EXTRA_COMPACT_ACTIONS)) {
+ mActionsToShowInCompact = extras.getIntArray(EXTRA_COMPACT_ACTIONS);
+ }
+ }
+
+ private RemoteViews generateMediaActionButton(Action action, int color) {
+ final boolean tombstone = (action.actionIntent == null);
+ RemoteViews button = new BuilderRemoteViews(mBuilder.mContext.getApplicationInfo(),
+ R.layout.notification_material_media_action);
+ button.setImageViewIcon(R.id.action0, action.getIcon());
+
+ // If the action buttons should not be tinted, then just use the default
+ // notification color. Otherwise, just use the passed-in color.
+ int tintColor = mBuilder.shouldTintActionButtons() || mBuilder.isColorized()
+ ? color
+ : NotificationColorUtil.resolveColor(mBuilder.mContext,
+ Notification.COLOR_DEFAULT);
+
+ button.setDrawableParameters(R.id.action0, false, -1, tintColor,
+ PorterDuff.Mode.SRC_ATOP, -1);
+ if (!tombstone) {
+ button.setOnClickPendingIntent(R.id.action0, action.actionIntent);
+ }
+ button.setContentDescription(R.id.action0, action.title);
+ return button;
+ }
+
+ private RemoteViews makeMediaContentView() {
+ RemoteViews view = mBuilder.applyStandardTemplate(
+ R.layout.notification_template_material_media, false /* hasProgress */);
+
+ final int numActions = mBuilder.mActions.size();
+ final int N = mActionsToShowInCompact == null
+ ? 0
+ : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
+ if (N > 0) {
+ view.removeAllViews(com.android.internal.R.id.media_actions);
+ for (int i = 0; i < N; i++) {
+ if (i >= numActions) {
+ throw new IllegalArgumentException(String.format(
+ "setShowActionsInCompactView: action %d out of bounds (max %d)",
+ i, numActions - 1));
+ }
+
+ final Action action = mBuilder.mActions.get(mActionsToShowInCompact[i]);
+ final RemoteViews button = generateMediaActionButton(action,
+ getPrimaryHighlightColor());
+ view.addView(com.android.internal.R.id.media_actions, button);
+ }
+ }
+ handleImage(view);
+ // handle the content margin
+ int endMargin = R.dimen.notification_content_margin_end;
+ if (mBuilder.mN.hasLargeIcon()) {
+ endMargin = R.dimen.notification_content_plus_picture_margin_end;
+ }
+ view.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
+ return view;
+ }
+
+ private int getPrimaryHighlightColor() {
+ return mBuilder.getPrimaryHighlightColor();
+ }
+
+ private RemoteViews makeMediaBigContentView() {
+ final int actionCount = Math.min(mBuilder.mActions.size(), MAX_MEDIA_BUTTONS);
+ // Dont add an expanded view if there is no more content to be revealed
+ int actionsInCompact = mActionsToShowInCompact == null
+ ? 0
+ : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
+ if (!mBuilder.mN.hasLargeIcon() && actionCount <= actionsInCompact) {
+ return null;
+ }
+ RemoteViews big = mBuilder.applyStandardTemplate(
+ R.layout.notification_template_material_big_media,
+ false);
+
+ if (actionCount > 0) {
+ big.removeAllViews(com.android.internal.R.id.media_actions);
+ for (int i = 0; i < actionCount; i++) {
+ final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i),
+ getPrimaryHighlightColor());
+ big.addView(com.android.internal.R.id.media_actions, button);
+ }
+ }
+ handleImage(big);
+ return big;
+ }
+
+ private void handleImage(RemoteViews contentView) {
+ if (mBuilder.mN.hasLargeIcon()) {
+ contentView.setViewLayoutMarginEndDimen(R.id.line1, 0);
+ contentView.setViewLayoutMarginEndDimen(R.id.text, 0);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected boolean hasProgress() {
+ return false;
+ }
+ }
+
+ /**
+ * Notification style for custom views that are decorated by the system
+ *
+ * <p>Instead of providing a notification that is completely custom, a developer can set this
+ * style and still obtain system decorations like the notification header with the expand
+ * affordance and actions.
+ *
+ * <p>Use {@link android.app.Notification.Builder#setCustomContentView(RemoteViews)},
+ * {@link android.app.Notification.Builder#setCustomBigContentView(RemoteViews)} and
+ * {@link android.app.Notification.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
+ * corresponding custom views to display.
+ *
+ * To use this style with your Notification, feed it to
+ * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.Builder()
+ * .setSmallIcon(R.drawable.ic_stat_player)
+ * .setLargeIcon(albumArtBitmap))
+ * .setCustomContentView(contentView);
+ * .setStyle(<b>new Notification.DecoratedCustomViewStyle()</b>)
+ * .build();
+ * </pre>
+ */
+ public static class DecoratedCustomViewStyle extends Style {
+
+ public DecoratedCustomViewStyle() {
+ }
+
+ /**
+ * @hide
+ */
+ public boolean displayCustomViewInline() {
+ return true;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ return makeStandardTemplateWithCustomContent(mBuilder.mN.contentView);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeBigContentView() {
+ return makeDecoratedBigContentView();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ return makeDecoratedHeadsUpContentView();
+ }
+
+ private RemoteViews makeDecoratedHeadsUpContentView() {
+ RemoteViews headsUpContentView = mBuilder.mN.headsUpContentView == null
+ ? mBuilder.mN.contentView
+ : mBuilder.mN.headsUpContentView;
+ if (mBuilder.mActions.size() == 0) {
+ return makeStandardTemplateWithCustomContent(headsUpContentView);
+ }
+ RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions(
+ mBuilder.getBigBaseLayoutResource());
+ buildIntoRemoteViewContent(remoteViews, headsUpContentView);
+ return remoteViews;
+ }
+
+ private RemoteViews makeStandardTemplateWithCustomContent(RemoteViews customContent) {
+ RemoteViews remoteViews = mBuilder.applyStandardTemplate(
+ mBuilder.getBaseLayoutResource());
+ buildIntoRemoteViewContent(remoteViews, customContent);
+ return remoteViews;
+ }
+
+ private RemoteViews makeDecoratedBigContentView() {
+ RemoteViews bigContentView = mBuilder.mN.bigContentView == null
+ ? mBuilder.mN.contentView
+ : mBuilder.mN.bigContentView;
+ if (mBuilder.mActions.size() == 0) {
+ return makeStandardTemplateWithCustomContent(bigContentView);
+ }
+ RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions(
+ mBuilder.getBigBaseLayoutResource());
+ buildIntoRemoteViewContent(remoteViews, bigContentView);
+ return remoteViews;
+ }
+
+ private void buildIntoRemoteViewContent(RemoteViews remoteViews,
+ RemoteViews customContent) {
+ if (customContent != null) {
+ // Need to clone customContent before adding, because otherwise it can no longer be
+ // parceled independently of remoteViews.
+ customContent = customContent.clone();
+ remoteViews.removeAllViewsExceptId(R.id.notification_main_column, R.id.progress);
+ remoteViews.addView(R.id.notification_main_column, customContent, 0 /* index */);
+ remoteViews.setReapplyDisallowed();
+ }
+ // also update the end margin if there is an image
+ int endMargin = R.dimen.notification_content_margin_end;
+ if (mBuilder.mN.hasLargeIcon()) {
+ endMargin = R.dimen.notification_content_plus_picture_margin_end;
+ }
+ remoteViews.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
+ }
+ }
+
+ /**
+ * Notification style for media custom views that are decorated by the system
+ *
+ * <p>Instead of providing a media notification that is completely custom, a developer can set
+ * this style and still obtain system decorations like the notification header with the expand
+ * affordance and actions.
+ *
+ * <p>Use {@link android.app.Notification.Builder#setCustomContentView(RemoteViews)},
+ * {@link android.app.Notification.Builder#setCustomBigContentView(RemoteViews)} and
+ * {@link android.app.Notification.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
+ * corresponding custom views to display.
+ * <p>
+ * Contrary to {@link MediaStyle} a developer has to opt-in to the colorizing of the
+ * notification by using {@link Notification.Builder#setColorized(boolean)}.
+ * <p>
+ * To use this style with your Notification, feed it to
+ * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so:
+ * <pre class="prettyprint">
+ * Notification noti = new Notification.Builder()
+ * .setSmallIcon(R.drawable.ic_stat_player)
+ * .setLargeIcon(albumArtBitmap))
+ * .setCustomContentView(contentView);
+ * .setStyle(<b>new Notification.DecoratedMediaCustomViewStyle()</b>
+ * .setMediaSession(mySession))
+ * .build();
+ * </pre>
+ *
+ * @see android.app.Notification.DecoratedCustomViewStyle
+ * @see android.app.Notification.MediaStyle
+ */
+ public static class DecoratedMediaCustomViewStyle extends MediaStyle {
+
+ public DecoratedMediaCustomViewStyle() {
+ }
+
+ /**
+ * @hide
+ */
+ public boolean displayCustomViewInline() {
+ return true;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeContentView(boolean increasedHeight) {
+ RemoteViews remoteViews = super.makeContentView(false /* increasedHeight */);
+ return buildIntoRemoteView(remoteViews, R.id.notification_content_container,
+ mBuilder.mN.contentView);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeBigContentView() {
+ RemoteViews customRemoteView = mBuilder.mN.bigContentView != null
+ ? mBuilder.mN.bigContentView
+ : mBuilder.mN.contentView;
+ return makeBigContentViewWithCustomContent(customRemoteView);
+ }
+
+ private RemoteViews makeBigContentViewWithCustomContent(RemoteViews customRemoteView) {
+ RemoteViews remoteViews = super.makeBigContentView();
+ if (remoteViews != null) {
+ return buildIntoRemoteView(remoteViews, R.id.notification_main_column,
+ customRemoteView);
+ } else if (customRemoteView != mBuilder.mN.contentView){
+ remoteViews = super.makeContentView(false /* increasedHeight */);
+ return buildIntoRemoteView(remoteViews, R.id.notification_content_container,
+ customRemoteView);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
+ RemoteViews customRemoteView = mBuilder.mN.headsUpContentView != null
+ ? mBuilder.mN.headsUpContentView
+ : mBuilder.mN.contentView;
+ return makeBigContentViewWithCustomContent(customRemoteView);
+ }
+
+ private RemoteViews buildIntoRemoteView(RemoteViews remoteViews, int id,
+ RemoteViews customContent) {
+ if (customContent != null) {
+ // Need to clone customContent before adding, because otherwise it can no longer be
+ // parceled independently of remoteViews.
+ customContent = customContent.clone();
+ customContent.overrideTextColors(mBuilder.getPrimaryTextColor());
+ remoteViews.removeAllViews(id);
+ remoteViews.addView(id, customContent);
+ remoteViews.setReapplyDisallowed();
+ }
+ return remoteViews;
+ }
+ }
+
+ // When adding a new Style subclass here, don't forget to update
+ // Builder.getNotificationStyleClass.
+
+ /**
+ * Extender interface for use with {@link Builder#extend}. Extenders may be used to add
+ * metadata or change options on a notification builder.
+ */
+ public interface Extender {
+ /**
+ * Apply this extender to a notification builder.
+ * @param builder the builder to be modified.
+ * @return the build object for chaining.
+ */
+ public Builder extend(Builder builder);
+ }
+
+ /**
+ * Helper class to add wearable extensions to notifications.
+ * <p class="note"> See
+ * <a href="{@docRoot}wear/notifications/creating.html">Creating Notifications
+ * for Android Wear</a> for more information on how to use this class.
+ * <p>
+ * To create a notification with wearable extensions:
+ * <ol>
+ * <li>Create a {@link android.app.Notification.Builder}, setting any desired
+ * properties.
+ * <li>Create a {@link android.app.Notification.WearableExtender}.
+ * <li>Set wearable-specific properties using the
+ * {@code add} and {@code set} methods of {@link android.app.Notification.WearableExtender}.
+ * <li>Call {@link android.app.Notification.Builder#extend} to apply the extensions to a
+ * notification.
+ * <li>Post the notification to the notification system with the
+ * {@code NotificationManager.notify(...)} methods.
+ * </ol>
+ *
+ * <pre class="prettyprint">
+ * Notification notif = new Notification.Builder(mContext)
+ * .setContentTitle(&quot;New mail from &quot; + sender.toString())
+ * .setContentText(subject)
+ * .setSmallIcon(R.drawable.new_mail)
+ * .extend(new Notification.WearableExtender()
+ * .setContentIcon(R.drawable.new_mail))
+ * .build();
+ * NotificationManager notificationManger =
+ * (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ * notificationManger.notify(0, notif);</pre>
+ *
+ * <p>Wearable extensions can be accessed on an existing notification by using the
+ * {@code WearableExtender(Notification)} constructor,
+ * and then using the {@code get} methods to access values.
+ *
+ * <pre class="prettyprint">
+ * Notification.WearableExtender wearableExtender = new Notification.WearableExtender(
+ * notification);
+ * List&lt;Notification&gt; pages = wearableExtender.getPages();</pre>
+ */
+ public static final class WearableExtender implements Extender {
+ /**
+ * Sentinel value for an action index that is unset.
+ */
+ public static final int UNSET_ACTION_INDEX = -1;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification with
+ * default sizing.
+ * <p>For custom display notifications created using {@link #setDisplayIntent},
+ * the default is {@link #SIZE_MEDIUM}. All other notifications size automatically based
+ * on their content.
+ */
+ public static final int SIZE_DEFAULT = 0;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with an extra small size.
+ * <p>This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ */
+ public static final int SIZE_XSMALL = 1;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a small size.
+ * <p>This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ */
+ public static final int SIZE_SMALL = 2;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a medium size.
+ * <p>This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ */
+ public static final int SIZE_MEDIUM = 3;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * with a large size.
+ * <p>This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ */
+ public static final int SIZE_LARGE = 4;
+
+ /**
+ * Size value for use with {@link #setCustomSizePreset} to show this notification
+ * full screen.
+ * <p>This value is only applicable for custom display notifications created using
+ * {@link #setDisplayIntent}.
+ */
+ public static final int SIZE_FULL_SCREEN = 5;
+
+ /**
+ * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a
+ * short amount of time when this notification is displayed on the screen. This
+ * is the default value.
+ */
+ public static final int SCREEN_TIMEOUT_SHORT = 0;
+
+ /**
+ * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on
+ * for a longer amount of time when this notification is displayed on the screen.
+ */
+ public static final int SCREEN_TIMEOUT_LONG = -1;
+
+ /** Notification extra which contains wearable extensions */
+ private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS";
+
+ // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options.
+ private static final String KEY_ACTIONS = "actions";
+ private static final String KEY_FLAGS = "flags";
+ private static final String KEY_DISPLAY_INTENT = "displayIntent";
+ private static final String KEY_PAGES = "pages";
+ private static final String KEY_BACKGROUND = "background";
+ private static final String KEY_CONTENT_ICON = "contentIcon";
+ private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity";
+ private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex";
+ private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset";
+ private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight";
+ private static final String KEY_GRAVITY = "gravity";
+ private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout";
+ private static final String KEY_DISMISSAL_ID = "dismissalId";
+ private static final String KEY_BRIDGE_TAG = "bridgeTag";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1;
+ private static final int FLAG_HINT_HIDE_ICON = 1 << 1;
+ private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2;
+ private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3;
+ private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4;
+ private static final int FLAG_BIG_PICTURE_AMBIENT = 1 << 5;
+ private static final int FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY = 1 << 6;
+
+ // Default value for flags integer
+ private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE;
+
+ private static final int DEFAULT_CONTENT_ICON_GRAVITY = Gravity.END;
+ private static final int DEFAULT_GRAVITY = Gravity.BOTTOM;
+
+ private ArrayList<Action> mActions = new ArrayList<Action>();
+ private int mFlags = DEFAULT_FLAGS;
+ private PendingIntent mDisplayIntent;
+ private ArrayList<Notification> mPages = new ArrayList<Notification>();
+ private Bitmap mBackground;
+ private int mContentIcon;
+ private int mContentIconGravity = DEFAULT_CONTENT_ICON_GRAVITY;
+ private int mContentActionIndex = UNSET_ACTION_INDEX;
+ private int mCustomSizePreset = SIZE_DEFAULT;
+ private int mCustomContentHeight;
+ private int mGravity = DEFAULT_GRAVITY;
+ private int mHintScreenTimeout;
+ private String mDismissalId;
+ private String mBridgeTag;
+
+ /**
+ * Create a {@link android.app.Notification.WearableExtender} with default
+ * options.
+ */
+ public WearableExtender() {
+ }
+
+ public WearableExtender(Notification notif) {
+ Bundle wearableBundle = notif.extras.getBundle(EXTRA_WEARABLE_EXTENSIONS);
+ if (wearableBundle != null) {
+ List<Action> actions = wearableBundle.getParcelableArrayList(KEY_ACTIONS);
+ if (actions != null) {
+ mActions.addAll(actions);
+ }
+
+ mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS);
+ mDisplayIntent = wearableBundle.getParcelable(KEY_DISPLAY_INTENT);
+
+ Notification[] pages = getNotificationArrayFromBundle(
+ wearableBundle, KEY_PAGES);
+ if (pages != null) {
+ Collections.addAll(mPages, pages);
+ }
+
+ mBackground = wearableBundle.getParcelable(KEY_BACKGROUND);
+ mContentIcon = wearableBundle.getInt(KEY_CONTENT_ICON);
+ mContentIconGravity = wearableBundle.getInt(KEY_CONTENT_ICON_GRAVITY,
+ DEFAULT_CONTENT_ICON_GRAVITY);
+ mContentActionIndex = wearableBundle.getInt(KEY_CONTENT_ACTION_INDEX,
+ UNSET_ACTION_INDEX);
+ mCustomSizePreset = wearableBundle.getInt(KEY_CUSTOM_SIZE_PRESET,
+ SIZE_DEFAULT);
+ mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT);
+ mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY);
+ mHintScreenTimeout = wearableBundle.getInt(KEY_HINT_SCREEN_TIMEOUT);
+ mDismissalId = wearableBundle.getString(KEY_DISMISSAL_ID);
+ mBridgeTag = wearableBundle.getString(KEY_BRIDGE_TAG);
+ }
+ }
+
+ /**
+ * Apply wearable extensions to a notification that is being built. This is typically
+ * called by the {@link android.app.Notification.Builder#extend} method of
+ * {@link android.app.Notification.Builder}.
+ */
+ @Override
+ public Notification.Builder extend(Notification.Builder builder) {
+ Bundle wearableBundle = new Bundle();
+
+ if (!mActions.isEmpty()) {
+ wearableBundle.putParcelableArrayList(KEY_ACTIONS, mActions);
+ }
+ if (mFlags != DEFAULT_FLAGS) {
+ wearableBundle.putInt(KEY_FLAGS, mFlags);
+ }
+ if (mDisplayIntent != null) {
+ wearableBundle.putParcelable(KEY_DISPLAY_INTENT, mDisplayIntent);
+ }
+ if (!mPages.isEmpty()) {
+ wearableBundle.putParcelableArray(KEY_PAGES, mPages.toArray(
+ new Notification[mPages.size()]));
+ }
+ if (mBackground != null) {
+ wearableBundle.putParcelable(KEY_BACKGROUND, mBackground);
+ }
+ if (mContentIcon != 0) {
+ wearableBundle.putInt(KEY_CONTENT_ICON, mContentIcon);
+ }
+ if (mContentIconGravity != DEFAULT_CONTENT_ICON_GRAVITY) {
+ wearableBundle.putInt(KEY_CONTENT_ICON_GRAVITY, mContentIconGravity);
+ }
+ if (mContentActionIndex != UNSET_ACTION_INDEX) {
+ wearableBundle.putInt(KEY_CONTENT_ACTION_INDEX,
+ mContentActionIndex);
+ }
+ if (mCustomSizePreset != SIZE_DEFAULT) {
+ wearableBundle.putInt(KEY_CUSTOM_SIZE_PRESET, mCustomSizePreset);
+ }
+ if (mCustomContentHeight != 0) {
+ wearableBundle.putInt(KEY_CUSTOM_CONTENT_HEIGHT, mCustomContentHeight);
+ }
+ if (mGravity != DEFAULT_GRAVITY) {
+ wearableBundle.putInt(KEY_GRAVITY, mGravity);
+ }
+ if (mHintScreenTimeout != 0) {
+ wearableBundle.putInt(KEY_HINT_SCREEN_TIMEOUT, mHintScreenTimeout);
+ }
+ if (mDismissalId != null) {
+ wearableBundle.putString(KEY_DISMISSAL_ID, mDismissalId);
+ }
+ if (mBridgeTag != null) {
+ wearableBundle.putString(KEY_BRIDGE_TAG, mBridgeTag);
+ }
+
+ builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle);
+ return builder;
+ }
+
+ @Override
+ public WearableExtender clone() {
+ WearableExtender that = new WearableExtender();
+ that.mActions = new ArrayList<Action>(this.mActions);
+ that.mFlags = this.mFlags;
+ that.mDisplayIntent = this.mDisplayIntent;
+ that.mPages = new ArrayList<Notification>(this.mPages);
+ that.mBackground = this.mBackground;
+ that.mContentIcon = this.mContentIcon;
+ that.mContentIconGravity = this.mContentIconGravity;
+ that.mContentActionIndex = this.mContentActionIndex;
+ that.mCustomSizePreset = this.mCustomSizePreset;
+ that.mCustomContentHeight = this.mCustomContentHeight;
+ that.mGravity = this.mGravity;
+ that.mHintScreenTimeout = this.mHintScreenTimeout;
+ that.mDismissalId = this.mDismissalId;
+ that.mBridgeTag = this.mBridgeTag;
+ return that;
+ }
+
+ /**
+ * Add a wearable action to this notification.
+ *
+ * <p>When wearable actions are added using this method, the set of actions that
+ * show on a wearable device splits from devices that only show actions added
+ * using {@link android.app.Notification.Builder#addAction}. This allows for customization
+ * of which actions display on different devices.
+ *
+ * @param action the action to add to this notification
+ * @return this object for method chaining
+ * @see android.app.Notification.Action
+ */
+ public WearableExtender addAction(Action action) {
+ mActions.add(action);
+ return this;
+ }
+
+ /**
+ * Adds wearable actions to this notification.
+ *
+ * <p>When wearable actions are added using this method, the set of actions that
+ * show on a wearable device splits from devices that only show actions added
+ * using {@link android.app.Notification.Builder#addAction}. This allows for customization
+ * of which actions display on different devices.
+ *
+ * @param actions the actions to add to this notification
+ * @return this object for method chaining
+ * @see android.app.Notification.Action
+ */
+ public WearableExtender addActions(List<Action> actions) {
+ mActions.addAll(actions);
+ return this;
+ }
+
+ /**
+ * Clear all wearable actions present on this builder.
+ * @return this object for method chaining.
+ * @see #addAction
+ */
+ public WearableExtender clearActions() {
+ mActions.clear();
+ return this;
+ }
+
+ /**
+ * Get the wearable actions present on this notification.
+ */
+ public List<Action> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Set an intent to launch inside of an activity view when displaying
+ * this notification. The {@link PendingIntent} provided should be for an activity.
+ *
+ * <pre class="prettyprint">
+ * Intent displayIntent = new Intent(context, MyDisplayActivity.class);
+ * PendingIntent displayPendingIntent = PendingIntent.getActivity(context,
+ * 0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ * Notification notif = new Notification.Builder(context)
+ * .extend(new Notification.WearableExtender()
+ * .setDisplayIntent(displayPendingIntent)
+ * .setCustomSizePreset(Notification.WearableExtender.SIZE_MEDIUM))
+ * .build();</pre>
+ *
+ * <p>The activity to launch needs to allow embedding, must be exported, and
+ * should have an empty task affinity. It is also recommended to use the device
+ * default light theme.
+ *
+ * <p>Example AndroidManifest.xml entry:
+ * <pre class="prettyprint">
+ * &lt;activity android:name=&quot;com.example.MyDisplayActivity&quot;
+ * android:exported=&quot;true&quot;
+ * android:allowEmbedded=&quot;true&quot;
+ * android:taskAffinity=&quot;&quot;
+ * android:theme=&quot;@android:style/Theme.DeviceDefault.Light&quot; /&gt;</pre>
+ *
+ * @param intent the {@link PendingIntent} for an activity
+ * @return this object for method chaining
+ * @see android.app.Notification.WearableExtender#getDisplayIntent
+ */
+ public WearableExtender setDisplayIntent(PendingIntent intent) {
+ mDisplayIntent = intent;
+ return this;
+ }
+
+ /**
+ * Get the intent to launch inside of an activity view when displaying this
+ * notification. This {@code PendingIntent} should be for an activity.
+ */
+ public PendingIntent getDisplayIntent() {
+ return mDisplayIntent;
+ }
+
+ /**
+ * Add an additional page of content to display with this notification. The current
+ * notification forms the first page, and pages added using this function form
+ * subsequent pages. This field can be used to separate a notification into multiple
+ * sections.
+ *
+ * @param page the notification to add as another page
+ * @return this object for method chaining
+ * @see android.app.Notification.WearableExtender#getPages
+ */
+ public WearableExtender addPage(Notification page) {
+ mPages.add(page);
+ return this;
+ }
+
+ /**
+ * Add additional pages of content to display with this notification. The current
+ * notification forms the first page, and pages added using this function form
+ * subsequent pages. This field can be used to separate a notification into multiple
+ * sections.
+ *
+ * @param pages a list of notifications
+ * @return this object for method chaining
+ * @see android.app.Notification.WearableExtender#getPages
+ */
+ public WearableExtender addPages(List<Notification> pages) {
+ mPages.addAll(pages);
+ return this;
+ }
+
+ /**
+ * Clear all additional pages present on this builder.
+ * @return this object for method chaining.
+ * @see #addPage
+ */
+ public WearableExtender clearPages() {
+ mPages.clear();
+ return this;
+ }
+
+ /**
+ * Get the array of additional pages of content for displaying this notification. The
+ * current notification forms the first page, and elements within this array form
+ * subsequent pages. This field can be used to separate a notification into multiple
+ * sections.
+ * @return the pages for this notification
+ */
+ public List<Notification> getPages() {
+ return mPages;
+ }
+
+ /**
+ * Set a background image to be displayed behind the notification content.
+ * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background
+ * will work with any notification style.
+ *
+ * @param background the background bitmap
+ * @return this object for method chaining
+ * @see android.app.Notification.WearableExtender#getBackground
+ */
+ public WearableExtender setBackground(Bitmap background) {
+ mBackground = background;
+ return this;
+ }
+
+ /**
+ * Get a background image to be displayed behind the notification content.
+ * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background
+ * will work with any notification style.
+ *
+ * @return the background image
+ * @see android.app.Notification.WearableExtender#setBackground
+ */
+ public Bitmap getBackground() {
+ return mBackground;
+ }
+
+ /**
+ * Set an icon that goes with the content of this notification.
+ */
+ public WearableExtender setContentIcon(int icon) {
+ mContentIcon = icon;
+ return this;
+ }
+
+ /**
+ * Get an icon that goes with the content of this notification.
+ */
+ public int getContentIcon() {
+ return mContentIcon;
+ }
+
+ /**
+ * Set the gravity that the content icon should have within the notification display.
+ * Supported values include {@link android.view.Gravity#START} and
+ * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}.
+ * @see #setContentIcon
+ */
+ public WearableExtender setContentIconGravity(int contentIconGravity) {
+ mContentIconGravity = contentIconGravity;
+ return this;
+ }
+
+ /**
+ * Get the gravity that the content icon should have within the notification display.
+ * Supported values include {@link android.view.Gravity#START} and
+ * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}.
+ * @see #getContentIcon
+ */
+ public int getContentIconGravity() {
+ return mContentIconGravity;
+ }
+
+ /**
+ * Set an action from this notification's actions to be clickable with the content of
+ * this notification. This action will no longer display separately from the
+ * notification's content.
+ *
+ * <p>For notifications with multiple pages, child pages can also have content actions
+ * set, although the list of available actions comes from the main notification and not
+ * from the child page's notification.
+ *
+ * @param actionIndex The index of the action to hoist onto the current notification page.
+ * If wearable actions were added to the main notification, this index
+ * will apply to that list, otherwise it will apply to the regular
+ * actions list.
+ */
+ public WearableExtender setContentAction(int actionIndex) {
+ mContentActionIndex = actionIndex;
+ return this;
+ }
+
+ /**
+ * Get the index of the notification action, if any, that was specified as being clickable
+ * with the content of this notification. This action will no longer display separately
+ * from the notification's content.
+ *
+ * <p>For notifications with multiple pages, child pages can also have content actions
+ * set, although the list of available actions comes from the main notification and not
+ * from the child page's notification.
+ *
+ * <p>If wearable specific actions were added to the main notification, this index will
+ * apply to that list, otherwise it will apply to the regular actions list.
+ *
+ * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected.
+ */
+ public int getContentAction() {
+ return mContentActionIndex;
+ }
+
+ /**
+ * Set the gravity that this notification should have within the available viewport space.
+ * Supported values include {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}.
+ * The default value is {@link android.view.Gravity#BOTTOM}.
+ */
+ public WearableExtender setGravity(int gravity) {
+ mGravity = gravity;
+ return this;
+ }
+
+ /**
+ * Get the gravity that this notification should have within the available viewport space.
+ * Supported values include {@link android.view.Gravity#TOP},
+ * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}.
+ * The default value is {@link android.view.Gravity#BOTTOM}.
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Set the custom size preset for the display of this notification out of the available
+ * presets found in {@link android.app.Notification.WearableExtender}, e.g.
+ * {@link #SIZE_LARGE}.
+ * <p>Some custom size presets are only applicable for custom display notifications created
+ * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. Check the
+ * documentation for the preset in question. See also
+ * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}.
+ */
+ public WearableExtender setCustomSizePreset(int sizePreset) {
+ mCustomSizePreset = sizePreset;
+ return this;
+ }
+
+ /**
+ * Get the custom size preset for the display of this notification out of the available
+ * presets found in {@link android.app.Notification.WearableExtender}, e.g.
+ * {@link #SIZE_LARGE}.
+ * <p>Some custom size presets are only applicable for custom display notifications created
+ * using {@link #setDisplayIntent}. Check the documentation for the preset in question.
+ * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}.
+ */
+ public int getCustomSizePreset() {
+ return mCustomSizePreset;
+ }
+
+ /**
+ * Set the custom height in pixels for the display of this notification's content.
+ * <p>This option is only available for custom display notifications created
+ * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. See also
+ * {@link android.app.Notification.WearableExtender#setCustomSizePreset} and
+ * {@link #getCustomContentHeight}.
+ */
+ public WearableExtender setCustomContentHeight(int height) {
+ mCustomContentHeight = height;
+ return this;
+ }
+
+ /**
+ * Get the custom height in pixels for the display of this notification's content.
+ * <p>This option is only available for custom display notifications created
+ * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and
+ * {@link #setCustomContentHeight}.
+ */
+ public int getCustomContentHeight() {
+ return mCustomContentHeight;
+ }
+
+ /**
+ * Set whether the scrolling position for the contents of this notification should start
+ * at the bottom of the contents instead of the top when the contents are too long to
+ * display within the screen. Default is false (start scroll at the top).
+ */
+ public WearableExtender setStartScrollBottom(boolean startScrollBottom) {
+ setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom);
+ return this;
+ }
+
+ /**
+ * Get whether the scrolling position for the contents of this notification should start
+ * at the bottom of the contents instead of the top when the contents are too long to
+ * display within the screen. Default is false (start scroll at the top).
+ */
+ public boolean getStartScrollBottom() {
+ return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0;
+ }
+
+ /**
+ * Set whether the content intent is available when the wearable device is not connected
+ * to a companion device. The user can still trigger this intent when the wearable device
+ * is offline, but a visual hint will indicate that the content intent may not be available.
+ * Defaults to true.
+ */
+ public WearableExtender setContentIntentAvailableOffline(
+ boolean contentIntentAvailableOffline) {
+ setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline);
+ return this;
+ }
+
+ /**
+ * Get whether the content intent is available when the wearable device is not connected
+ * to a companion device. The user can still trigger this intent when the wearable device
+ * is offline, but a visual hint will indicate that the content intent may not be available.
+ * Defaults to true.
+ */
+ public boolean getContentIntentAvailableOffline() {
+ return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's icon should not be displayed.
+ * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintHideIcon(boolean hintHideIcon) {
+ setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's icon should not be displayed.
+ * @return {@code true} if this icon should not be displayed, false otherwise.
+ * The default value is {@code false} if this was never set.
+ */
+ public boolean getHintHideIcon() {
+ return (mFlags & FLAG_HINT_HIDE_ICON) != 0;
+ }
+
+ /**
+ * Set a visual hint that only the background image of this notification should be
+ * displayed, and other semantic content should be hidden. This hint is only applicable
+ * to sub-pages added using {@link #addPage}.
+ */
+ public WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) {
+ setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly);
+ return this;
+ }
+
+ /**
+ * Get a visual hint that only the background image of this notification should be
+ * displayed, and other semantic content should be hidden. This hint is only applicable
+ * to sub-pages added using {@link android.app.Notification.WearableExtender#addPage}.
+ */
+ public boolean getHintShowBackgroundOnly() {
+ return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's background should not be clipped if possible,
+ * and should instead be resized to fully display on the screen, retaining the aspect
+ * ratio of the image. This can be useful for images like barcodes or qr codes.
+ * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintAvoidBackgroundClipping(
+ boolean hintAvoidBackgroundClipping) {
+ setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's background should not be clipped if possible,
+ * and should instead be resized to fully display on the screen, retaining the aspect
+ * ratio of the image. This can be useful for images like barcodes or qr codes.
+ * @return {@code true} if it's ok if the background is clipped on the screen, false
+ * otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintAvoidBackgroundClipping() {
+ return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0;
+ }
+
+ /**
+ * Set a hint that the screen should remain on for at least this duration when
+ * this notification is displayed on the screen.
+ * @param timeout The requested screen timeout in milliseconds. Can also be either
+ * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintScreenTimeout(int timeout) {
+ mHintScreenTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Get the duration, in milliseconds, that the screen should remain on for
+ * when this notification is displayed.
+ * @return the duration in milliseconds if > 0, or either one of the sentinel values
+ * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}.
+ */
+ public int getHintScreenTimeout() {
+ return mHintScreenTimeout;
+ }
+
+ /**
+ * Set a hint that this notification's {@link BigPictureStyle} (if present) should be
+ * converted to low-bit and displayed in ambient mode, especially useful for barcodes and
+ * qr codes, as well as other simple black-and-white tickets.
+ * @param hintAmbientBigPicture {@code true} to enable converstion and ambient.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintAmbientBigPicture(boolean hintAmbientBigPicture) {
+ setFlag(FLAG_BIG_PICTURE_AMBIENT, hintAmbientBigPicture);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's {@link BigPictureStyle} (if present) should be
+ * converted to low-bit and displayed in ambient mode, especially useful for barcodes and
+ * qr codes, as well as other simple black-and-white tickets.
+ * @return {@code true} if it should be displayed in ambient, false otherwise
+ * otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintAmbientBigPicture() {
+ return (mFlags & FLAG_BIG_PICTURE_AMBIENT) != 0;
+ }
+
+ /**
+ * Set a hint that this notification's content intent will launch an {@link Activity}
+ * directly, telling the platform that it can generate the appropriate transitions.
+ * @param hintContentIntentLaunchesActivity {@code true} if the content intent will launch
+ * an activity and transitions should be generated, false otherwise.
+ * @return this object for method chaining
+ */
+ public WearableExtender setHintContentIntentLaunchesActivity(
+ boolean hintContentIntentLaunchesActivity) {
+ setFlag(FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY, hintContentIntentLaunchesActivity);
+ return this;
+ }
+
+ /**
+ * Get a hint that this notification's content intent will launch an {@link Activity}
+ * directly, telling the platform that it can generate the appropriate transitions
+ * @return {@code true} if the content intent will launch an activity and transitions should
+ * be generated, false otherwise. The default value is {@code false} if this was never set.
+ */
+ public boolean getHintContentIntentLaunchesActivity() {
+ return (mFlags & FLAG_HINT_CONTENT_INTENT_LAUNCHES_ACTIVITY) != 0;
+ }
+
+ /**
+ * Sets the dismissal id for this notification. If a notification is posted with a
+ * dismissal id, then when that notification is canceled, notifications on other wearables
+ * and the paired Android phone having that same dismissal id will also be canceled. See
+ * <a href="{@docRoot}wear/notifications/index.html">Adding Wearable Features to
+ * Notifications</a> for more information.
+ * @param dismissalId the dismissal id of the notification.
+ * @return this object for method chaining
+ */
+ public WearableExtender setDismissalId(String dismissalId) {
+ mDismissalId = dismissalId;
+ return this;
+ }
+
+ /**
+ * Returns the dismissal id of the notification.
+ * @return the dismissal id of the notification or null if it has not been set.
+ */
+ public String getDismissalId() {
+ return mDismissalId;
+ }
+
+ /**
+ * Sets a bridge tag for this notification. A bridge tag can be set for notifications
+ * posted from a phone to provide finer-grained control on what notifications are bridged
+ * to wearables. See <a href="{@docRoot}wear/notifications/index.html">Adding Wearable
+ * Features to Notifications</a> for more information.
+ * @param bridgeTag the bridge tag of the notification.
+ * @return this object for method chaining
+ */
+ public WearableExtender setBridgeTag(String bridgeTag) {
+ mBridgeTag = bridgeTag;
+ return this;
+ }
+
+ /**
+ * Returns the bridge tag of the notification.
+ * @return the bridge tag or null if not present.
+ */
+ public String getBridgeTag() {
+ return mBridgeTag;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+ }
+
+ /**
+ * <p>Helper class to add Android Auto extensions to notifications. To create a notification
+ * with car extensions:
+ *
+ * <ol>
+ * <li>Create an {@link Notification.Builder}, setting any desired
+ * properties.
+ * <li>Create a {@link CarExtender}.
+ * <li>Set car-specific properties using the {@code add} and {@code set} methods of
+ * {@link CarExtender}.
+ * <li>Call {@link Notification.Builder#extend(Notification.Extender)}
+ * to apply the extensions to a notification.
+ * </ol>
+ *
+ * <pre class="prettyprint">
+ * Notification notification = new Notification.Builder(context)
+ * ...
+ * .extend(new CarExtender()
+ * .set*(...))
+ * .build();
+ * </pre>
+ *
+ * <p>Car extensions can be accessed on an existing notification by using the
+ * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods
+ * to access values.
+ */
+ public static final class CarExtender implements Extender {
+ private static final String TAG = "CarExtender";
+
+ private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
+ private static final String EXTRA_LARGE_ICON = "large_icon";
+ private static final String EXTRA_CONVERSATION = "car_conversation";
+ private static final String EXTRA_COLOR = "app_color";
+
+ private Bitmap mLargeIcon;
+ private UnreadConversation mUnreadConversation;
+ private int mColor = Notification.COLOR_DEFAULT;
+
+ /**
+ * Create a {@link CarExtender} with default options.
+ */
+ public CarExtender() {
+ }
+
+ /**
+ * Create a {@link CarExtender} from the CarExtender options of an existing Notification.
+ *
+ * @param notif The notification from which to copy options.
+ */
+ public CarExtender(Notification notif) {
+ Bundle carBundle = notif.extras == null ?
+ null : notif.extras.getBundle(EXTRA_CAR_EXTENDER);
+ if (carBundle != null) {
+ mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON);
+ mColor = carBundle.getInt(EXTRA_COLOR, Notification.COLOR_DEFAULT);
+
+ Bundle b = carBundle.getBundle(EXTRA_CONVERSATION);
+ mUnreadConversation = UnreadConversation.getUnreadConversationFromBundle(b);
+ }
+ }
+
+ /**
+ * Apply car extensions to a notification that is being built. This is typically called by
+ * the {@link Notification.Builder#extend(Notification.Extender)}
+ * method of {@link Notification.Builder}.
+ */
+ @Override
+ public Notification.Builder extend(Notification.Builder builder) {
+ Bundle carExtensions = new Bundle();
+
+ if (mLargeIcon != null) {
+ carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon);
+ }
+ if (mColor != Notification.COLOR_DEFAULT) {
+ carExtensions.putInt(EXTRA_COLOR, mColor);
+ }
+
+ if (mUnreadConversation != null) {
+ Bundle b = mUnreadConversation.getBundleForUnreadConversation();
+ carExtensions.putBundle(EXTRA_CONVERSATION, b);
+ }
+
+ builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions);
+ return builder;
+ }
+
+ /**
+ * Sets the accent color to use when Android Auto presents the notification.
+ *
+ * Android Auto uses the color set with {@link Notification.Builder#setColor(int)}
+ * to accent the displayed notification. However, not all colors are acceptable in an
+ * automotive setting. This method can be used to override the color provided in the
+ * notification in such a situation.
+ */
+ public CarExtender setColor(@ColorInt int color) {
+ mColor = color;
+ return this;
+ }
+
+ /**
+ * Gets the accent color.
+ *
+ * @see #setColor
+ */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Sets the large icon of the car notification.
+ *
+ * If no large icon is set in the extender, Android Auto will display the icon
+ * specified by {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap)}
+ *
+ * @param largeIcon The large icon to use in the car notification.
+ * @return This object for method chaining.
+ */
+ public CarExtender setLargeIcon(Bitmap largeIcon) {
+ mLargeIcon = largeIcon;
+ return this;
+ }
+
+ /**
+ * Gets the large icon used in this car notification, or null if no icon has been set.
+ *
+ * @return The large icon for the car notification.
+ * @see CarExtender#setLargeIcon
+ */
+ public Bitmap getLargeIcon() {
+ return mLargeIcon;
+ }
+
+ /**
+ * Sets the unread conversation in a message notification.
+ *
+ * @param unreadConversation The unread part of the conversation this notification conveys.
+ * @return This object for method chaining.
+ */
+ public CarExtender setUnreadConversation(UnreadConversation unreadConversation) {
+ mUnreadConversation = unreadConversation;
+ return this;
+ }
+
+ /**
+ * Returns the unread conversation conveyed by this notification.
+ * @see #setUnreadConversation(UnreadConversation)
+ */
+ public UnreadConversation getUnreadConversation() {
+ return mUnreadConversation;
+ }
+
+ /**
+ * A class which holds the unread messages from a conversation.
+ */
+ public static class UnreadConversation {
+ private static final String KEY_AUTHOR = "author";
+ private static final String KEY_TEXT = "text";
+ private static final String KEY_MESSAGES = "messages";
+ private static final String KEY_REMOTE_INPUT = "remote_input";
+ private static final String KEY_ON_REPLY = "on_reply";
+ private static final String KEY_ON_READ = "on_read";
+ private static final String KEY_PARTICIPANTS = "participants";
+ private static final String KEY_TIMESTAMP = "timestamp";
+
+ private final String[] mMessages;
+ private final RemoteInput mRemoteInput;
+ private final PendingIntent mReplyPendingIntent;
+ private final PendingIntent mReadPendingIntent;
+ private final String[] mParticipants;
+ private final long mLatestTimestamp;
+
+ UnreadConversation(String[] messages, RemoteInput remoteInput,
+ PendingIntent replyPendingIntent, PendingIntent readPendingIntent,
+ String[] participants, long latestTimestamp) {
+ mMessages = messages;
+ mRemoteInput = remoteInput;
+ mReadPendingIntent = readPendingIntent;
+ mReplyPendingIntent = replyPendingIntent;
+ mParticipants = participants;
+ mLatestTimestamp = latestTimestamp;
+ }
+
+ /**
+ * Gets the list of messages conveyed by this notification.
+ */
+ public String[] getMessages() {
+ return mMessages;
+ }
+
+ /**
+ * Gets the remote input that will be used to convey the response to a message list, or
+ * null if no such remote input exists.
+ */
+ public RemoteInput getRemoteInput() {
+ return mRemoteInput;
+ }
+
+ /**
+ * Gets the pending intent that will be triggered when the user replies to this
+ * notification.
+ */
+ public PendingIntent getReplyPendingIntent() {
+ return mReplyPendingIntent;
+ }
+
+ /**
+ * Gets the pending intent that Android Auto will send after it reads aloud all messages
+ * in this object's message list.
+ */
+ public PendingIntent getReadPendingIntent() {
+ return mReadPendingIntent;
+ }
+
+ /**
+ * Gets the participants in the conversation.
+ */
+ public String[] getParticipants() {
+ return mParticipants;
+ }
+
+ /**
+ * Gets the firs participant in the conversation.
+ */
+ public String getParticipant() {
+ return mParticipants.length > 0 ? mParticipants[0] : null;
+ }
+
+ /**
+ * Gets the timestamp of the conversation.
+ */
+ public long getLatestTimestamp() {
+ return mLatestTimestamp;
+ }
+
+ Bundle getBundleForUnreadConversation() {
+ Bundle b = new Bundle();
+ String author = null;
+ if (mParticipants != null && mParticipants.length > 1) {
+ author = mParticipants[0];
+ }
+ Parcelable[] messages = new Parcelable[mMessages.length];
+ for (int i = 0; i < messages.length; i++) {
+ Bundle m = new Bundle();
+ m.putString(KEY_TEXT, mMessages[i]);
+ m.putString(KEY_AUTHOR, author);
+ messages[i] = m;
+ }
+ b.putParcelableArray(KEY_MESSAGES, messages);
+ if (mRemoteInput != null) {
+ b.putParcelable(KEY_REMOTE_INPUT, mRemoteInput);
+ }
+ b.putParcelable(KEY_ON_REPLY, mReplyPendingIntent);
+ b.putParcelable(KEY_ON_READ, mReadPendingIntent);
+ b.putStringArray(KEY_PARTICIPANTS, mParticipants);
+ b.putLong(KEY_TIMESTAMP, mLatestTimestamp);
+ return b;
+ }
+
+ static UnreadConversation getUnreadConversationFromBundle(Bundle b) {
+ if (b == null) {
+ return null;
+ }
+ Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES);
+ String[] messages = null;
+ if (parcelableMessages != null) {
+ String[] tmp = new String[parcelableMessages.length];
+ boolean success = true;
+ for (int i = 0; i < tmp.length; i++) {
+ if (!(parcelableMessages[i] instanceof Bundle)) {
+ success = false;
+ break;
+ }
+ tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT);
+ if (tmp[i] == null) {
+ success = false;
+ break;
+ }
+ }
+ if (success) {
+ messages = tmp;
+ } else {
+ return null;
+ }
+ }
+
+ PendingIntent onRead = b.getParcelable(KEY_ON_READ);
+ PendingIntent onReply = b.getParcelable(KEY_ON_REPLY);
+
+ RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT);
+
+ String[] participants = b.getStringArray(KEY_PARTICIPANTS);
+ if (participants == null || participants.length != 1) {
+ return null;
+ }
+
+ return new UnreadConversation(messages,
+ remoteInput,
+ onReply,
+ onRead,
+ participants, b.getLong(KEY_TIMESTAMP));
+ }
+ };
+
+ /**
+ * Builder class for {@link CarExtender.UnreadConversation} objects.
+ */
+ public static class Builder {
+ private final List<String> mMessages = new ArrayList<String>();
+ private final String mParticipant;
+ private RemoteInput mRemoteInput;
+ private PendingIntent mReadPendingIntent;
+ private PendingIntent mReplyPendingIntent;
+ private long mLatestTimestamp;
+
+ /**
+ * Constructs a new builder for {@link CarExtender.UnreadConversation}.
+ *
+ * @param name The name of the other participant in the conversation.
+ */
+ public Builder(String name) {
+ mParticipant = name;
+ }
+
+ /**
+ * Appends a new unread message to the list of messages for this conversation.
+ *
+ * The messages should be added from oldest to newest.
+ *
+ * @param message The text of the new unread message.
+ * @return This object for method chaining.
+ */
+ public Builder addMessage(String message) {
+ mMessages.add(message);
+ return this;
+ }
+
+ /**
+ * Sets the pending intent and remote input which will convey the reply to this
+ * notification.
+ *
+ * @param pendingIntent The pending intent which will be triggered on a reply.
+ * @param remoteInput The remote input parcelable which will carry the reply.
+ * @return This object for method chaining.
+ *
+ * @see CarExtender.UnreadConversation#getRemoteInput
+ * @see CarExtender.UnreadConversation#getReplyPendingIntent
+ */
+ public Builder setReplyAction(
+ PendingIntent pendingIntent, RemoteInput remoteInput) {
+ mRemoteInput = remoteInput;
+ mReplyPendingIntent = pendingIntent;
+
+ return this;
+ }
+
+ /**
+ * Sets the pending intent that will be sent once the messages in this notification
+ * are read.
+ *
+ * @param pendingIntent The pending intent to use.
+ * @return This object for method chaining.
+ */
+ public Builder setReadPendingIntent(PendingIntent pendingIntent) {
+ mReadPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
+ * Sets the timestamp of the most recent message in an unread conversation.
+ *
+ * If a messaging notification has been posted by your application and has not
+ * yet been cancelled, posting a later notification with the same id and tag
+ * but without a newer timestamp may result in Android Auto not displaying a
+ * heads up notification for the later notification.
+ *
+ * @param timestamp The timestamp of the most recent message in the conversation.
+ * @return This object for method chaining.
+ */
+ public Builder setLatestTimestamp(long timestamp) {
+ mLatestTimestamp = timestamp;
+ return this;
+ }
+
+ /**
+ * Builds a new unread conversation object.
+ *
+ * @return The new unread conversation object.
+ */
+ public UnreadConversation build() {
+ String[] messages = mMessages.toArray(new String[mMessages.size()]);
+ String[] participants = { mParticipant };
+ return new UnreadConversation(messages, mRemoteInput, mReplyPendingIntent,
+ mReadPendingIntent, participants, mLatestTimestamp);
+ }
+ }
+ }
+
+ /**
+ * <p>Helper class to add Android TV extensions to notifications. To create a notification
+ * with a TV extension:
+ *
+ * <ol>
+ * <li>Create an {@link Notification.Builder}, setting any desired properties.
+ * <li>Create a {@link TvExtender}.
+ * <li>Set TV-specific properties using the {@code set} methods of
+ * {@link TvExtender}.
+ * <li>Call {@link Notification.Builder#extend(Notification.Extender)}
+ * to apply the extension to a notification.
+ * </ol>
+ *
+ * <pre class="prettyprint">
+ * Notification notification = new Notification.Builder(context)
+ * ...
+ * .extend(new TvExtender()
+ * .set*(...))
+ * .build();
+ * </pre>
+ *
+ * <p>TV extensions can be accessed on an existing notification by using the
+ * {@code TvExtender(Notification)} constructor, and then using the {@code get} methods
+ * to access values.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class TvExtender implements Extender {
+ private static final String TAG = "TvExtender";
+
+ private static final String EXTRA_TV_EXTENDER = "android.tv.EXTENSIONS";
+ private static final String EXTRA_FLAGS = "flags";
+ private static final String EXTRA_CONTENT_INTENT = "content_intent";
+ private static final String EXTRA_DELETE_INTENT = "delete_intent";
+ private static final String EXTRA_CHANNEL_ID = "channel_id";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_AVAILABLE_ON_TV = 0x1;
+
+ private int mFlags;
+ private String mChannelId;
+ private PendingIntent mContentIntent;
+ private PendingIntent mDeleteIntent;
+
+ /**
+ * Create a {@link TvExtender} with default options.
+ */
+ public TvExtender() {
+ mFlags = FLAG_AVAILABLE_ON_TV;
+ }
+
+ /**
+ * Create a {@link TvExtender} from the TvExtender options of an existing Notification.
+ *
+ * @param notif The notification from which to copy options.
+ */
+ public TvExtender(Notification notif) {
+ Bundle bundle = notif.extras == null ?
+ null : notif.extras.getBundle(EXTRA_TV_EXTENDER);
+ if (bundle != null) {
+ mFlags = bundle.getInt(EXTRA_FLAGS);
+ mChannelId = bundle.getString(EXTRA_CHANNEL_ID);
+ mContentIntent = bundle.getParcelable(EXTRA_CONTENT_INTENT);
+ mDeleteIntent = bundle.getParcelable(EXTRA_DELETE_INTENT);
+ }
+ }
+
+ /**
+ * Apply a TV extension to a notification that is being built. This is typically called by
+ * the {@link Notification.Builder#extend(Notification.Extender)}
+ * method of {@link Notification.Builder}.
+ */
+ @Override
+ public Notification.Builder extend(Notification.Builder builder) {
+ Bundle bundle = new Bundle();
+
+ bundle.putInt(EXTRA_FLAGS, mFlags);
+ bundle.putString(EXTRA_CHANNEL_ID, mChannelId);
+ if (mContentIntent != null) {
+ bundle.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent);
+ }
+
+ if (mDeleteIntent != null) {
+ bundle.putParcelable(EXTRA_DELETE_INTENT, mDeleteIntent);
+ }
+
+ builder.getExtras().putBundle(EXTRA_TV_EXTENDER, bundle);
+ return builder;
+ }
+
+ /**
+ * Returns true if this notification should be shown on TV. This method return true
+ * if the notification was extended with a TvExtender.
+ */
+ public boolean isAvailableOnTv() {
+ return (mFlags & FLAG_AVAILABLE_ON_TV) != 0;
+ }
+
+ /**
+ * Specifies the channel the notification should be delivered on when shown on TV.
+ * It can be different from the channel that the notification is delivered to when
+ * posting on a non-TV device.
+ */
+ public TvExtender setChannel(String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ /**
+ * Specifies the channel the notification should be delivered on when shown on TV.
+ * It can be different from the channel that the notification is delivered to when
+ * posting on a non-TV device.
+ */
+ public TvExtender setChannelId(String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ /** @removed */
+ @Deprecated
+ public String getChannel() {
+ return mChannelId;
+ }
+
+ /**
+ * Returns the id of the channel this notification posts to on TV.
+ */
+ public String getChannelId() {
+ return mChannelId;
+ }
+
+ /**
+ * Supplies a {@link PendingIntent} to be sent when the notification is selected on TV.
+ * If provided, it is used instead of the content intent specified
+ * at the level of Notification.
+ */
+ public TvExtender setContentIntent(PendingIntent intent) {
+ mContentIntent = intent;
+ return this;
+ }
+
+ /**
+ * Returns the TV-specific content intent. If this method returns null, the
+ * main content intent on the notification should be used.
+ *
+ * @see {@link Notification#contentIntent}
+ */
+ public PendingIntent getContentIntent() {
+ return mContentIntent;
+ }
+
+ /**
+ * Supplies a {@link PendingIntent} to send when the notification is cleared explicitly
+ * by the user on TV. If provided, it is used instead of the delete intent specified
+ * at the level of Notification.
+ */
+ public TvExtender setDeleteIntent(PendingIntent intent) {
+ mDeleteIntent = intent;
+ return this;
+ }
+
+ /**
+ * Returns the TV-specific delete intent. If this method returns null, the
+ * main delete intent on the notification should be used.
+ *
+ * @see {@link Notification#deleteIntent}
+ */
+ public PendingIntent getDeleteIntent() {
+ return mDeleteIntent;
+ }
+ }
+
+ /**
+ * Get an array of Notification objects from a parcelable array bundle field.
+ * Update the bundle to have a typed array so fetches in the future don't need
+ * to do an array copy.
+ */
+ private static Notification[] getNotificationArrayFromBundle(Bundle bundle, String key) {
+ Parcelable[] array = bundle.getParcelableArray(key);
+ if (array instanceof Notification[] || array == null) {
+ return (Notification[]) array;
+ }
+ Notification[] typedArray = Arrays.copyOf(array, array.length,
+ Notification[].class);
+ bundle.putParcelableArray(key, typedArray);
+ return typedArray;
+ }
+
+ private static class BuilderRemoteViews extends RemoteViews {
+ public BuilderRemoteViews(Parcel parcel) {
+ super(parcel);
+ }
+
+ public BuilderRemoteViews(ApplicationInfo appInfo, int layoutId) {
+ super(appInfo, layoutId);
+ }
+
+ @Override
+ public BuilderRemoteViews clone() {
+ Parcel p = Parcel.obtain();
+ writeToParcel(p, 0);
+ p.setDataPosition(0);
+ BuilderRemoteViews brv = new BuilderRemoteViews(p);
+ p.recycle();
+ return brv;
+ }
+ }
+
+ private static class StandardTemplateParams {
+ boolean hasProgress = true;
+ boolean ambient = false;
+ CharSequence title;
+ CharSequence text;
+
+ final StandardTemplateParams reset() {
+ hasProgress = true;
+ ambient = false;
+ title = null;
+ text = null;
+ return this;
+ }
+
+ final StandardTemplateParams hasProgress(boolean hasProgress) {
+ this.hasProgress = hasProgress;
+ return this;
+ }
+
+ final StandardTemplateParams title(CharSequence title) {
+ this.title = title;
+ return this;
+ }
+
+ final StandardTemplateParams text(CharSequence text) {
+ this.text = text;
+ return this;
+ }
+
+ final StandardTemplateParams ambient(boolean ambient) {
+ Preconditions.checkState(title == null && text == null, "must set ambient before text");
+ this.ambient = ambient;
+ return this;
+ }
+
+ final StandardTemplateParams fillTextsFrom(Builder b) {
+ Bundle extras = b.mN.extras;
+ title = b.processLegacyText(extras.getCharSequence(EXTRA_TITLE), ambient);
+ text = b.processLegacyText(extras.getCharSequence(EXTRA_TEXT), ambient);
+ return this;
+ }
+ }
+}
diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java
new file mode 100644
index 00000000..163a8dca
--- /dev/null
+++ b/android/app/NotificationChannel.java
@@ -0,0 +1,853 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.annotation.SystemApi;
+import android.app.NotificationManager.Importance;
+import android.content.Intent;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A representation of settings that apply to a collection of similarly themed notifications.
+ */
+public final class NotificationChannel implements Parcelable {
+
+ /**
+ * The id of the default channel for an app. This id is reserved by the system. All
+ * notifications posted from apps targeting {@link android.os.Build.VERSION_CODES#N_MR1} or
+ * earlier without a notification channel specified are posted to this channel.
+ */
+ public static final String DEFAULT_CHANNEL_ID = "miscellaneous";
+
+ /**
+ * The maximum length for text fields in a NotificationChannel. Fields will be truncated at this
+ * limit.
+ */
+ private static final int MAX_TEXT_LENGTH = 1000;
+
+ private static final String TAG_CHANNEL = "channel";
+ private static final String ATT_NAME = "name";
+ private static final String ATT_DESC = "desc";
+ private static final String ATT_ID = "id";
+ private static final String ATT_DELETED = "deleted";
+ private static final String ATT_PRIORITY = "priority";
+ private static final String ATT_VISIBILITY = "visibility";
+ private static final String ATT_IMPORTANCE = "importance";
+ private static final String ATT_LIGHTS = "lights";
+ private static final String ATT_LIGHT_COLOR = "light_color";
+ private static final String ATT_VIBRATION = "vibration";
+ private static final String ATT_VIBRATION_ENABLED = "vibration_enabled";
+ private static final String ATT_SOUND = "sound";
+ private static final String ATT_USAGE = "usage";
+ private static final String ATT_FLAGS = "flags";
+ private static final String ATT_CONTENT_TYPE = "content_type";
+ private static final String ATT_SHOW_BADGE = "show_badge";
+ private static final String ATT_USER_LOCKED = "locked";
+ private static final String ATT_GROUP = "group";
+ private static final String ATT_BLOCKABLE_SYSTEM = "blockable_system";
+ private static final String DELIMITER = ",";
+
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_PRIORITY = 0x00000001;
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_VISIBILITY = 0x00000002;
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_IMPORTANCE = 0x00000004;
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_LIGHTS = 0x00000008;
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_VIBRATION = 0x00000010;
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_SOUND = 0x00000020;
+
+ /**
+ * @hide
+ */
+ public static final int USER_LOCKED_SHOW_BADGE = 0x00000080;
+
+ /**
+ * @hide
+ */
+ public static final int[] LOCKABLE_FIELDS = new int[] {
+ USER_LOCKED_PRIORITY,
+ USER_LOCKED_VISIBILITY,
+ USER_LOCKED_IMPORTANCE,
+ USER_LOCKED_LIGHTS,
+ USER_LOCKED_VIBRATION,
+ USER_LOCKED_SOUND,
+ USER_LOCKED_SHOW_BADGE,
+ };
+
+ private static final int DEFAULT_LIGHT_COLOR = 0;
+ private static final int DEFAULT_VISIBILITY =
+ NotificationManager.VISIBILITY_NO_OVERRIDE;
+ private static final int DEFAULT_IMPORTANCE =
+ NotificationManager.IMPORTANCE_UNSPECIFIED;
+ private static final boolean DEFAULT_DELETED = false;
+ private static final boolean DEFAULT_SHOW_BADGE = true;
+
+ private final String mId;
+ private String mName;
+ private String mDesc;
+ private int mImportance = DEFAULT_IMPORTANCE;
+ private boolean mBypassDnd;
+ private int mLockscreenVisibility = DEFAULT_VISIBILITY;
+ private Uri mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
+ private boolean mLights;
+ private int mLightColor = DEFAULT_LIGHT_COLOR;
+ private long[] mVibration;
+ private int mUserLockedFields;
+ private boolean mVibrationEnabled;
+ private boolean mShowBadge = DEFAULT_SHOW_BADGE;
+ private boolean mDeleted = DEFAULT_DELETED;
+ private String mGroup;
+ private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
+ private boolean mBlockableSystem = false;
+
+ /**
+ * Creates a notification channel.
+ *
+ * @param id The id of the channel. Must be unique per package. The value may be truncated if
+ * it is too long.
+ * @param name The user visible name of the channel. You can rename this channel when the system
+ * locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
+ * broadcast. The recommended maximum length is 40 characters; the value may be
+ * truncated if it is too long.
+ * @param importance The importance of the channel. This controls how interruptive notifications
+ * posted to this channel are.
+ */
+ public NotificationChannel(String id, CharSequence name, @Importance int importance) {
+ this.mId = getTrimmedString(id);
+ this.mName = name != null ? getTrimmedString(name.toString()) : null;
+ this.mImportance = importance;
+ }
+
+ /**
+ * @hide
+ */
+ protected NotificationChannel(Parcel in) {
+ if (in.readByte() != 0) {
+ mId = in.readString();
+ } else {
+ mId = null;
+ }
+ if (in.readByte() != 0) {
+ mName = in.readString();
+ } else {
+ mName = null;
+ }
+ if (in.readByte() != 0) {
+ mDesc = in.readString();
+ } else {
+ mDesc = null;
+ }
+ mImportance = in.readInt();
+ mBypassDnd = in.readByte() != 0;
+ mLockscreenVisibility = in.readInt();
+ if (in.readByte() != 0) {
+ mSound = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mSound = null;
+ }
+ mLights = in.readByte() != 0;
+ mVibration = in.createLongArray();
+ mUserLockedFields = in.readInt();
+ mVibrationEnabled = in.readByte() != 0;
+ mShowBadge = in.readByte() != 0;
+ mDeleted = in.readByte() != 0;
+ if (in.readByte() != 0) {
+ mGroup = in.readString();
+ } else {
+ mGroup = null;
+ }
+ mAudioAttributes = in.readInt() > 0 ? AudioAttributes.CREATOR.createFromParcel(in) : null;
+ mLightColor = in.readInt();
+ mBlockableSystem = in.readBoolean();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mId != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mId);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mName != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mName);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mDesc != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mDesc);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeInt(mImportance);
+ dest.writeByte(mBypassDnd ? (byte) 1 : (byte) 0);
+ dest.writeInt(mLockscreenVisibility);
+ if (mSound != null) {
+ dest.writeByte((byte) 1);
+ mSound.writeToParcel(dest, 0);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeByte(mLights ? (byte) 1 : (byte) 0);
+ dest.writeLongArray(mVibration);
+ dest.writeInt(mUserLockedFields);
+ dest.writeByte(mVibrationEnabled ? (byte) 1 : (byte) 0);
+ dest.writeByte(mShowBadge ? (byte) 1 : (byte) 0);
+ dest.writeByte(mDeleted ? (byte) 1 : (byte) 0);
+ if (mGroup != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mGroup);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mAudioAttributes != null) {
+ dest.writeInt(1);
+ mAudioAttributes.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(mLightColor);
+ dest.writeBoolean(mBlockableSystem);
+ }
+
+ /**
+ * @hide
+ */
+ public void lockFields(int field) {
+ mUserLockedFields |= field;
+ }
+
+ /**
+ * @hide
+ */
+ public void unlockFields(int field) {
+ mUserLockedFields &= ~field;
+ }
+
+ /**
+ * @hide
+ */
+ public void setDeleted(boolean deleted) {
+ mDeleted = deleted;
+ }
+
+ /**
+ * @hide
+ */
+ public void setBlockableSystem(boolean blockableSystem) {
+ mBlockableSystem = blockableSystem;
+ }
+ // Modifiable by apps post channel creation
+
+ /**
+ * Sets the user visible name of this channel.
+ *
+ * <p>The recommended maximum length is 40 characters; the value may be truncated if it is too
+ * long.
+ */
+ public void setName(CharSequence name) {
+ mName = name != null ? getTrimmedString(name.toString()) : null;
+ }
+
+ /**
+ * Sets the user visible description of this channel.
+ *
+ * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too
+ * long.
+ */
+ public void setDescription(String description) {
+ mDesc = getTrimmedString(description);
+ }
+
+ private String getTrimmedString(String input) {
+ if (input != null && input.length() > MAX_TEXT_LENGTH) {
+ return input.substring(0, MAX_TEXT_LENGTH);
+ }
+ return input;
+ }
+
+ // Modifiable by apps on channel creation.
+
+ /**
+ * Sets what group this channel belongs to.
+ *
+ * Group information is only used for presentation, not for behavior.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ *
+ * @param groupId the id of a group created by
+ * {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.
+ */
+ public void setGroup(String groupId) {
+ this.mGroup = groupId;
+ }
+
+ /**
+ * Sets whether notifications posted to this channel can appear as application icon badges
+ * in a Launcher.
+ *
+ * @param showBadge true if badges should be allowed to be shown.
+ */
+ public void setShowBadge(boolean showBadge) {
+ this.mShowBadge = showBadge;
+ }
+
+ /**
+ * Sets the sound that should be played for notifications posted to this channel and its
+ * audio attributes. Notification channels with an {@link #getImportance() importance} of at
+ * least {@link NotificationManager#IMPORTANCE_DEFAULT} should have a sound.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ */
+ public void setSound(Uri sound, AudioAttributes audioAttributes) {
+ this.mSound = sound;
+ this.mAudioAttributes = audioAttributes;
+ }
+
+ /**
+ * Sets whether notifications posted to this channel should display notification lights,
+ * on devices that support that feature.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ */
+ public void enableLights(boolean lights) {
+ this.mLights = lights;
+ }
+
+ /**
+ * Sets the notification light color for notifications posted to this channel, if lights are
+ * {@link #enableLights(boolean) enabled} on this channel and the device supports that feature.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ */
+ public void setLightColor(int argb) {
+ this.mLightColor = argb;
+ }
+
+ /**
+ * Sets whether notification posted to this channel should vibrate. The vibration pattern can
+ * be set with {@link #setVibrationPattern(long[])}.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ */
+ public void enableVibration(boolean vibration) {
+ this.mVibrationEnabled = vibration;
+ }
+
+ /**
+ * Sets the vibration pattern for notifications posted to this channel. If the provided
+ * pattern is valid (non-null, non-empty), will {@link #enableVibration(boolean)} enable
+ * vibration} as well. Otherwise, vibration will be disabled.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ */
+ public void setVibrationPattern(long[] vibrationPattern) {
+ this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0;
+ this.mVibration = vibrationPattern;
+ }
+
+ /**
+ * Sets the level of interruption of this notification channel. Only
+ * modifiable before the channel is submitted to
+ * {@link NotificationManager#notify(String, int, Notification)}.
+ *
+ * @param importance the amount the user should be interrupted by
+ * notifications from this channel.
+ */
+ public void setImportance(@Importance int importance) {
+ this.mImportance = importance;
+ }
+
+ // Modifiable by a notification ranker.
+
+ /**
+ * Sets whether or not notifications posted to this channel can interrupt the user in
+ * {@link android.app.NotificationManager.Policy#INTERRUPTION_FILTER_PRIORITY} mode.
+ *
+ * Only modifiable by the system and notification ranker.
+ */
+ public void setBypassDnd(boolean bypassDnd) {
+ this.mBypassDnd = bypassDnd;
+ }
+
+ /**
+ * Sets whether notifications posted to this channel appear on the lockscreen or not, and if so,
+ * whether they appear in a redacted form. See e.g. {@link Notification#VISIBILITY_SECRET}.
+ *
+ * Only modifiable by the system and notification ranker.
+ */
+ public void setLockscreenVisibility(int lockscreenVisibility) {
+ this.mLockscreenVisibility = lockscreenVisibility;
+ }
+
+ /**
+ * Returns the id of this channel.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the user visible name of this channel.
+ */
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the user visible description of this channel.
+ */
+ public String getDescription() {
+ return mDesc;
+ }
+
+ /**
+ * Returns the user specified importance e.g. {@link NotificationManager#IMPORTANCE_LOW} for
+ * notifications posted to this channel.
+ */
+ public int getImportance() {
+ return mImportance;
+ }
+
+ /**
+ * Whether or not notifications posted to this channel can bypass the Do Not Disturb
+ * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY} mode.
+ */
+ public boolean canBypassDnd() {
+ return mBypassDnd;
+ }
+
+ /**
+ * Returns the notification sound for this channel.
+ */
+ public Uri getSound() {
+ return mSound;
+ }
+
+ /**
+ * Returns the audio attributes for sound played by notifications posted to this channel.
+ */
+ public AudioAttributes getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ /**
+ * Returns whether notifications posted to this channel trigger notification lights.
+ */
+ public boolean shouldShowLights() {
+ return mLights;
+ }
+
+ /**
+ * Returns the notification light color for notifications posted to this channel. Irrelevant
+ * unless {@link #shouldShowLights()}.
+ */
+ public int getLightColor() {
+ return mLightColor;
+ }
+
+ /**
+ * Returns whether notifications posted to this channel always vibrate.
+ */
+ public boolean shouldVibrate() {
+ return mVibrationEnabled;
+ }
+
+ /**
+ * Returns the vibration pattern for notifications posted to this channel. Will be ignored if
+ * vibration is not enabled ({@link #shouldVibrate()}.
+ */
+ public long[] getVibrationPattern() {
+ return mVibration;
+ }
+
+ /**
+ * Returns whether or not notifications posted to this channel are shown on the lockscreen in
+ * full or redacted form.
+ */
+ public int getLockscreenVisibility() {
+ return mLockscreenVisibility;
+ }
+
+ /**
+ * Returns whether notifications posted to this channel can appear as badges in a Launcher
+ * application.
+ *
+ * Note that badging may be disabled for other reasons.
+ */
+ public boolean canShowBadge() {
+ return mShowBadge;
+ }
+
+ /**
+ * Returns what group this channel belongs to.
+ *
+ * This is used only for visually grouping channels in the UI.
+ */
+ public String getGroup() {
+ return mGroup;
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public boolean isDeleted() {
+ return mDeleted;
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public int getUserLockedFields() {
+ return mUserLockedFields;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isBlockableSystem() {
+ return mBlockableSystem;
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public void populateFromXml(XmlPullParser parser) {
+ // Name, id, and importance are set in the constructor.
+ setDescription(parser.getAttributeValue(null, ATT_DESC));
+ setBypassDnd(Notification.PRIORITY_DEFAULT
+ != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT));
+ setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
+ setSound(safeUri(parser, ATT_SOUND), safeAudioAttributes(parser));
+ enableLights(safeBool(parser, ATT_LIGHTS, false));
+ setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR));
+ setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null));
+ enableVibration(safeBool(parser, ATT_VIBRATION_ENABLED, false));
+ setShowBadge(safeBool(parser, ATT_SHOW_BADGE, false));
+ setDeleted(safeBool(parser, ATT_DELETED, false));
+ setGroup(parser.getAttributeValue(null, ATT_GROUP));
+ lockFields(safeInt(parser, ATT_USER_LOCKED, 0));
+ setBlockableSystem(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false));
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public void writeXml(XmlSerializer out) throws IOException {
+ out.startTag(null, TAG_CHANNEL);
+ out.attribute(null, ATT_ID, getId());
+ if (getName() != null) {
+ out.attribute(null, ATT_NAME, getName().toString());
+ }
+ if (getDescription() != null) {
+ out.attribute(null, ATT_DESC, getDescription());
+ }
+ if (getImportance() != DEFAULT_IMPORTANCE) {
+ out.attribute(
+ null, ATT_IMPORTANCE, Integer.toString(getImportance()));
+ }
+ if (canBypassDnd()) {
+ out.attribute(
+ null, ATT_PRIORITY, Integer.toString(Notification.PRIORITY_MAX));
+ }
+ if (getLockscreenVisibility() != DEFAULT_VISIBILITY) {
+ out.attribute(null, ATT_VISIBILITY,
+ Integer.toString(getLockscreenVisibility()));
+ }
+ if (getSound() != null) {
+ out.attribute(null, ATT_SOUND, getSound().toString());
+ }
+ if (getAudioAttributes() != null) {
+ out.attribute(null, ATT_USAGE, Integer.toString(getAudioAttributes().getUsage()));
+ out.attribute(null, ATT_CONTENT_TYPE,
+ Integer.toString(getAudioAttributes().getContentType()));
+ out.attribute(null, ATT_FLAGS, Integer.toString(getAudioAttributes().getFlags()));
+ }
+ if (shouldShowLights()) {
+ out.attribute(null, ATT_LIGHTS, Boolean.toString(shouldShowLights()));
+ }
+ if (getLightColor() != DEFAULT_LIGHT_COLOR) {
+ out.attribute(null, ATT_LIGHT_COLOR, Integer.toString(getLightColor()));
+ }
+ if (shouldVibrate()) {
+ out.attribute(null, ATT_VIBRATION_ENABLED, Boolean.toString(shouldVibrate()));
+ }
+ if (getVibrationPattern() != null) {
+ out.attribute(null, ATT_VIBRATION, longArrayToString(getVibrationPattern()));
+ }
+ if (getUserLockedFields() != 0) {
+ out.attribute(null, ATT_USER_LOCKED, Integer.toString(getUserLockedFields()));
+ }
+ if (canShowBadge()) {
+ out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(canShowBadge()));
+ }
+ if (isDeleted()) {
+ out.attribute(null, ATT_DELETED, Boolean.toString(isDeleted()));
+ }
+ if (getGroup() != null) {
+ out.attribute(null, ATT_GROUP, getGroup());
+ }
+ if (isBlockableSystem()) {
+ out.attribute(null, ATT_BLOCKABLE_SYSTEM, Boolean.toString(isBlockableSystem()));
+ }
+
+ out.endTag(null, TAG_CHANNEL);
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public JSONObject toJson() throws JSONException {
+ JSONObject record = new JSONObject();
+ record.put(ATT_ID, getId());
+ record.put(ATT_NAME, getName());
+ record.put(ATT_DESC, getDescription());
+ if (getImportance() != DEFAULT_IMPORTANCE) {
+ record.put(ATT_IMPORTANCE,
+ NotificationListenerService.Ranking.importanceToString(getImportance()));
+ }
+ if (canBypassDnd()) {
+ record.put(ATT_PRIORITY, Notification.PRIORITY_MAX);
+ }
+ if (getLockscreenVisibility() != DEFAULT_VISIBILITY) {
+ record.put(ATT_VISIBILITY, Notification.visibilityToString(getLockscreenVisibility()));
+ }
+ if (getSound() != null) {
+ record.put(ATT_SOUND, getSound().toString());
+ }
+ if (getAudioAttributes() != null) {
+ record.put(ATT_USAGE, Integer.toString(getAudioAttributes().getUsage()));
+ record.put(ATT_CONTENT_TYPE,
+ Integer.toString(getAudioAttributes().getContentType()));
+ record.put(ATT_FLAGS, Integer.toString(getAudioAttributes().getFlags()));
+ }
+ record.put(ATT_LIGHTS, Boolean.toString(shouldShowLights()));
+ record.put(ATT_LIGHT_COLOR, Integer.toString(getLightColor()));
+ record.put(ATT_VIBRATION_ENABLED, Boolean.toString(shouldVibrate()));
+ record.put(ATT_USER_LOCKED, Integer.toString(getUserLockedFields()));
+ record.put(ATT_VIBRATION, longArrayToString(getVibrationPattern()));
+ record.put(ATT_SHOW_BADGE, Boolean.toString(canShowBadge()));
+ record.put(ATT_DELETED, Boolean.toString(isDeleted()));
+ record.put(ATT_GROUP, getGroup());
+ record.put(ATT_BLOCKABLE_SYSTEM, isBlockableSystem());
+ return record;
+ }
+
+ private static AudioAttributes safeAudioAttributes(XmlPullParser parser) {
+ int usage = safeInt(parser, ATT_USAGE, AudioAttributes.USAGE_NOTIFICATION);
+ int contentType = safeInt(parser, ATT_CONTENT_TYPE,
+ AudioAttributes.CONTENT_TYPE_SONIFICATION);
+ int flags = safeInt(parser, ATT_FLAGS, 0);
+ return new AudioAttributes.Builder()
+ .setUsage(usage)
+ .setContentType(contentType)
+ .setFlags(flags)
+ .build();
+ }
+
+ private static Uri safeUri(XmlPullParser parser, String att) {
+ final String val = parser.getAttributeValue(null, att);
+ return val == null ? null : Uri.parse(val);
+ }
+
+ private static int safeInt(XmlPullParser parser, String att, int defValue) {
+ final String val = parser.getAttributeValue(null, att);
+ return tryParseInt(val, defValue);
+ }
+
+ private static int tryParseInt(String value, int defValue) {
+ if (TextUtils.isEmpty(value)) return defValue;
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
+ final String value = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(value)) return defValue;
+ return Boolean.parseBoolean(value);
+ }
+
+ private static long[] safeLongArray(XmlPullParser parser, String att, long[] defValue) {
+ final String attributeValue = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(attributeValue)) return defValue;
+ String[] values = attributeValue.split(DELIMITER);
+ long[] longValues = new long[values.length];
+ for (int i = 0; i < values.length; i++) {
+ try {
+ longValues[i] = Long.parseLong(values[i]);
+ } catch (NumberFormatException e) {
+ longValues[i] = 0;
+ }
+ }
+ return longValues;
+ }
+
+ private static String longArrayToString(long[] values) {
+ StringBuffer sb = new StringBuffer();
+ if (values != null && values.length > 0) {
+ for (int i = 0; i < values.length - 1; i++) {
+ sb.append(values[i]).append(DELIMITER);
+ }
+ sb.append(values[values.length - 1]);
+ }
+ return sb.toString();
+ }
+
+ public static final Creator<NotificationChannel> CREATOR = new Creator<NotificationChannel>() {
+ @Override
+ public NotificationChannel createFromParcel(Parcel in) {
+ return new NotificationChannel(in);
+ }
+
+ @Override
+ public NotificationChannel[] newArray(int size) {
+ return new NotificationChannel[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NotificationChannel that = (NotificationChannel) o;
+
+ if (getImportance() != that.getImportance()) return false;
+ if (mBypassDnd != that.mBypassDnd) return false;
+ if (getLockscreenVisibility() != that.getLockscreenVisibility()) return false;
+ if (mLights != that.mLights) return false;
+ if (getLightColor() != that.getLightColor()) return false;
+ if (getUserLockedFields() != that.getUserLockedFields()) return false;
+ if (mVibrationEnabled != that.mVibrationEnabled) return false;
+ if (mShowBadge != that.mShowBadge) return false;
+ if (isDeleted() != that.isDeleted()) return false;
+ if (isBlockableSystem() != that.isBlockableSystem()) return false;
+ if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
+ if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
+ return false;
+ }
+ if (getDescription() != null ? !getDescription().equals(that.getDescription())
+ : that.getDescription() != null) {
+ return false;
+ }
+ if (getSound() != null ? !getSound().equals(that.getSound()) : that.getSound() != null) {
+ return false;
+ }
+ if (!Arrays.equals(mVibration, that.mVibration)) return false;
+ if (getGroup() != null ? !getGroup().equals(that.getGroup()) : that.getGroup() != null) {
+ return false;
+ }
+ return getAudioAttributes() != null ? getAudioAttributes().equals(that.getAudioAttributes())
+ : that.getAudioAttributes() == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getId() != null ? getId().hashCode() : 0;
+ result = 31 * result + (getName() != null ? getName().hashCode() : 0);
+ result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
+ result = 31 * result + getImportance();
+ result = 31 * result + (mBypassDnd ? 1 : 0);
+ result = 31 * result + getLockscreenVisibility();
+ result = 31 * result + (getSound() != null ? getSound().hashCode() : 0);
+ result = 31 * result + (mLights ? 1 : 0);
+ result = 31 * result + getLightColor();
+ result = 31 * result + Arrays.hashCode(mVibration);
+ result = 31 * result + getUserLockedFields();
+ result = 31 * result + (mVibrationEnabled ? 1 : 0);
+ result = 31 * result + (mShowBadge ? 1 : 0);
+ result = 31 * result + (isDeleted() ? 1 : 0);
+ result = 31 * result + (getGroup() != null ? getGroup().hashCode() : 0);
+ result = 31 * result + (getAudioAttributes() != null ? getAudioAttributes().hashCode() : 0);
+ result = 31 * result + (isBlockableSystem() ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "NotificationChannel{"
+ + "mId='" + mId + '\''
+ + ", mName=" + mName
+ + ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "")
+ + ", mImportance=" + mImportance
+ + ", mBypassDnd=" + mBypassDnd
+ + ", mLockscreenVisibility=" + mLockscreenVisibility
+ + ", mSound=" + mSound
+ + ", mLights=" + mLights
+ + ", mLightColor=" + mLightColor
+ + ", mVibration=" + Arrays.toString(mVibration)
+ + ", mUserLockedFields=" + Integer.toHexString(mUserLockedFields)
+ + ", mVibrationEnabled=" + mVibrationEnabled
+ + ", mShowBadge=" + mShowBadge
+ + ", mDeleted=" + mDeleted
+ + ", mGroup='" + mGroup + '\''
+ + ", mAudioAttributes=" + mAudioAttributes
+ + ", mBlockableSystem=" + mBlockableSystem
+ + '}';
+ }
+}
diff --git a/android/app/NotificationChannelGroup.java b/android/app/NotificationChannelGroup.java
new file mode 100644
index 00000000..51733114
--- /dev/null
+++ b/android/app/NotificationChannelGroup.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A grouping of related notification channels. e.g., channels that all belong to a single account.
+ */
+public final class NotificationChannelGroup implements Parcelable {
+
+ /**
+ * The maximum length for text fields in a NotificationChannelGroup. Fields will be truncated at
+ * this limit.
+ */
+ private static final int MAX_TEXT_LENGTH = 1000;
+
+ private static final String TAG_GROUP = "channelGroup";
+ private static final String ATT_NAME = "name";
+ private static final String ATT_DESC = "desc";
+ private static final String ATT_ID = "id";
+ private static final String ATT_BLOCKED = "blocked";
+
+ private final String mId;
+ private CharSequence mName;
+ private String mDescription;
+ private boolean mBlocked;
+ private List<NotificationChannel> mChannels = new ArrayList<>();
+
+ /**
+ * Creates a notification channel group.
+ *
+ * @param id The id of the group. Must be unique per package. the value may be truncated if
+ * it is too long.
+ * @param name The user visible name of the group. You can rename this group when the system
+ * locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
+ * broadcast. <p>The recommended maximum length is 40 characters; the value may be
+ * truncated if it is too long.
+ */
+ public NotificationChannelGroup(String id, CharSequence name) {
+ this.mId = getTrimmedString(id);
+ this.mName = name != null ? getTrimmedString(name.toString()) : null;
+ }
+
+ /**
+ * @hide
+ */
+ protected NotificationChannelGroup(Parcel in) {
+ if (in.readByte() != 0) {
+ mId = in.readString();
+ } else {
+ mId = null;
+ }
+ mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ if (in.readByte() != 0) {
+ mDescription = in.readString();
+ } else {
+ mDescription = null;
+ }
+ in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader());
+ mBlocked = in.readBoolean();
+ }
+
+ private String getTrimmedString(String input) {
+ if (input != null && input.length() > MAX_TEXT_LENGTH) {
+ return input.substring(0, MAX_TEXT_LENGTH);
+ }
+ return input;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mId != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mId);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ TextUtils.writeToParcel(mName, dest, flags);
+ if (mDescription != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mDescription);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeParcelableList(mChannels, flags);
+ dest.writeBoolean(mBlocked);
+ }
+
+ /**
+ * Returns the id of this group.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the user visible name of this group.
+ */
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the user visible description of this group.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the list of channels that belong to this group
+ */
+ public List<NotificationChannel> getChannels() {
+ return mChannels;
+ }
+
+ /**
+ * Returns whether or not notifications posted to {@link NotificationChannel channels} belonging
+ * to this group are blocked.
+ */
+ public boolean isBlocked() {
+ return mBlocked;
+ }
+
+ /**
+ * Sets the user visible description of this group.
+ *
+ * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too
+ * long.
+ */
+ public void setDescription(String description) {
+ mDescription = getTrimmedString(description);
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void setBlocked(boolean blocked) {
+ mBlocked = blocked;
+ }
+
+ /**
+ * @hide
+ */
+ public void addChannel(NotificationChannel channel) {
+ mChannels.add(channel);
+ }
+
+ /**
+ * @hide
+ */
+ public void setChannels(List<NotificationChannel> channels) {
+ mChannels = channels;
+ }
+
+ /**
+ * @hide
+ */
+ public void populateFromXml(XmlPullParser parser) {
+ // Name, id, and importance are set in the constructor.
+ setDescription(parser.getAttributeValue(null, ATT_DESC));
+ setBlocked(safeBool(parser, ATT_BLOCKED, false));
+ }
+
+ private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
+ final String value = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(value)) return defValue;
+ return Boolean.parseBoolean(value);
+ }
+
+ /**
+ * @hide
+ */
+ public void writeXml(XmlSerializer out) throws IOException {
+ out.startTag(null, TAG_GROUP);
+
+ out.attribute(null, ATT_ID, getId());
+ if (getName() != null) {
+ out.attribute(null, ATT_NAME, getName().toString());
+ }
+ if (getDescription() != null) {
+ out.attribute(null, ATT_DESC, getDescription().toString());
+ }
+ out.attribute(null, ATT_BLOCKED, Boolean.toString(isBlocked()));
+
+ out.endTag(null, TAG_GROUP);
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public JSONObject toJson() throws JSONException {
+ JSONObject record = new JSONObject();
+ record.put(ATT_ID, getId());
+ record.put(ATT_NAME, getName());
+ record.put(ATT_DESC, getDescription());
+ record.put(ATT_BLOCKED, isBlocked());
+ return record;
+ }
+
+ public static final Creator<NotificationChannelGroup> CREATOR =
+ new Creator<NotificationChannelGroup>() {
+ @Override
+ public NotificationChannelGroup createFromParcel(Parcel in) {
+ return new NotificationChannelGroup(in);
+ }
+
+ @Override
+ public NotificationChannelGroup[] newArray(int size) {
+ return new NotificationChannelGroup[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NotificationChannelGroup that = (NotificationChannelGroup) o;
+
+ if (isBlocked() != that.isBlocked()) return false;
+ if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
+ if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
+ return false;
+ }
+ if (getDescription() != null ? !getDescription().equals(that.getDescription())
+ : that.getDescription() != null) {
+ return false;
+ }
+ return getChannels() != null ? getChannels().equals(that.getChannels())
+ : that.getChannels() == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getId() != null ? getId().hashCode() : 0;
+ result = 31 * result + (getName() != null ? getName().hashCode() : 0);
+ result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
+ result = 31 * result + (isBlocked() ? 1 : 0);
+ result = 31 * result + (getChannels() != null ? getChannels().hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public NotificationChannelGroup clone() {
+ NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName());
+ cloned.setDescription(getDescription());
+ cloned.setBlocked(isBlocked());
+ cloned.setChannels(getChannels());
+ return cloned;
+ }
+
+ @Override
+ public String toString() {
+ return "NotificationChannelGroup{"
+ + "mId='" + mId + '\''
+ + ", mName=" + mName
+ + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "")
+ + ", mBlocked=" + mBlocked
+ + ", mChannels=" + mChannels
+ + '}';
+ }
+}
diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java
new file mode 100644
index 00000000..8fa7d6c3
--- /dev/null
+++ b/android/app/NotificationManager.java
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.app.Notification.Builder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.provider.Settings.Global;
+import android.service.notification.StatusBarNotification;
+import android.service.notification.ZenModeConfig;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class to notify the user of events that happen. This is how you tell
+ * the user that something has happened in the background. {@more}
+ *
+ * Notifications can take different forms:
+ * <ul>
+ * <li>A persistent icon that goes in the status bar and is accessible
+ * through the launcher, (when the user selects it, a designated Intent
+ * can be launched),</li>
+ * <li>Turning on or flashing LEDs on the device, or</li>
+ * <li>Alerting the user by flashing the backlight, playing a sound,
+ * or vibrating.</li>
+ * </ul>
+ *
+ * <p>
+ * Each of the notify methods takes an int id parameter and optionally a
+ * {@link String} tag parameter, which may be {@code null}. These parameters
+ * are used to form a pair (tag, id), or ({@code null}, id) if tag is
+ * unspecified. This pair identifies this notification from your app to the
+ * system, so that pair should be unique within your app. If you call one
+ * of the notify methods with a (tag, id) pair that is currently active and
+ * a new set of notification parameters, it will be updated. For example,
+ * if you pass a new status bar icon, the old icon in the status bar will
+ * be replaced with the new one. This is also the same tag and id you pass
+ * to the {@link #cancel(int)} or {@link #cancel(String, int)} method to clear
+ * this notification.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a guide to creating notifications, read the
+ * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html">Status Bar Notifications</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * @see android.app.Notification
+ */
+@SystemService(Context.NOTIFICATION_SERVICE)
+public class NotificationManager {
+ private static String TAG = "NotificationManager";
+ private static boolean localLOGV = false;
+
+ /**
+ * Intent that is broadcast when the state of {@link #getEffectsSuppressor()} changes.
+ * This broadcast is only sent to registered receivers.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_EFFECTS_SUPPRESSOR_CHANGED
+ = "android.os.action.ACTION_EFFECTS_SUPPRESSOR_CHANGED";
+
+ /**
+ * Intent that is broadcast when the state of {@link #isNotificationPolicyAccessGranted()}
+ * changes.
+ *
+ * This broadcast is only sent to registered receivers, and only to the apps that have changed.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED
+ = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
+
+ /**
+ * Intent that is broadcast when the state of getNotificationPolicy() changes.
+ * This broadcast is only sent to registered receivers.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NOTIFICATION_POLICY_CHANGED
+ = "android.app.action.NOTIFICATION_POLICY_CHANGED";
+
+ /**
+ * Intent that is broadcast when the state of getCurrentInterruptionFilter() changes.
+ * This broadcast is only sent to registered receivers.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_INTERRUPTION_FILTER_CHANGED
+ = "android.app.action.INTERRUPTION_FILTER_CHANGED";
+
+ /**
+ * Intent that is broadcast when the state of getCurrentInterruptionFilter() changes.
+ * @hide
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL
+ = "android.app.action.INTERRUPTION_FILTER_CHANGED_INTERNAL";
+
+ /** @hide */
+ @IntDef(prefix = { "INTERRUPTION_FILTER_" }, value = {
+ INTERRUPTION_FILTER_NONE, INTERRUPTION_FILTER_PRIORITY, INTERRUPTION_FILTER_ALARMS,
+ INTERRUPTION_FILTER_ALL, INTERRUPTION_FILTER_UNKNOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InterruptionFilter {}
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Normal interruption filter - no notifications are suppressed.
+ */
+ public static final int INTERRUPTION_FILTER_ALL = 1;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Priority interruption filter - all notifications are suppressed except those that match
+ * the priority criteria. Some audio streams are muted. See
+ * {@link Policy#priorityCallSenders}, {@link Policy#priorityCategories},
+ * {@link Policy#priorityMessageSenders} to define or query this criteria. Users can
+ * additionally specify packages that can bypass this interruption filter.
+ */
+ public static final int INTERRUPTION_FILTER_PRIORITY = 2;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * No interruptions filter - all notifications are suppressed and all audio streams (except
+ * those used for phone calls) and vibrations are muted.
+ */
+ public static final int INTERRUPTION_FILTER_NONE = 3;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Alarms only interruption filter - all notifications except those of category
+ * {@link Notification#CATEGORY_ALARM} are suppressed. Some audio streams are muted.
+ */
+ public static final int INTERRUPTION_FILTER_ALARMS = 4;
+
+ /** {@link #getCurrentInterruptionFilter() Interruption filter} constant - returned when
+ * the value is unavailable for any reason.
+ */
+ public static final int INTERRUPTION_FILTER_UNKNOWN = 0;
+
+ /** @hide */
+ @IntDef(prefix = { "IMPORTANCE_" }, value = {
+ IMPORTANCE_UNSPECIFIED, IMPORTANCE_NONE,
+ IMPORTANCE_MIN, IMPORTANCE_LOW, IMPORTANCE_DEFAULT, IMPORTANCE_HIGH
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Importance {}
+
+ /** Value signifying that the user has not expressed a per-app visibility override value.
+ * @hide */
+ public static final int VISIBILITY_NO_OVERRIDE = -1000;
+
+ /**
+ * Value signifying that the user has not expressed an importance.
+ *
+ * This value is for persisting preferences, and should never be associated with
+ * an actual notification.
+ */
+ public static final int IMPORTANCE_UNSPECIFIED = -1000;
+
+ /**
+ * A notification with no importance: does not show in the shade.
+ */
+ public static final int IMPORTANCE_NONE = 0;
+
+ /**
+ * Min notification importance: only shows in the shade, below the fold. This should
+ * not be used with {@link Service#startForeground(int, Notification) Service.startForeground}
+ * since a foreground service is supposed to be something the user cares about so it does
+ * not make semantic sense to mark its notification as minimum importance. If you do this
+ * as of Android version {@link android.os.Build.VERSION_CODES#O}, the system will show
+ * a higher-priority notification about your app running in the background.
+ */
+ public static final int IMPORTANCE_MIN = 1;
+
+ /**
+ * Low notification importance: shows everywhere, but is not intrusive.
+ */
+ public static final int IMPORTANCE_LOW = 2;
+
+ /**
+ * Default notification importance: shows everywhere, makes noise, but does not visually
+ * intrude.
+ */
+ public static final int IMPORTANCE_DEFAULT = 3;
+
+ /**
+ * Higher notification importance: shows everywhere, makes noise and peeks. May use full screen
+ * intents.
+ */
+ public static final int IMPORTANCE_HIGH = 4;
+
+ /**
+ * Unused.
+ */
+ public static final int IMPORTANCE_MAX = 5;
+
+ private static INotificationManager sService;
+
+ /** @hide */
+ static public INotificationManager getService()
+ {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService("notification");
+ sService = INotificationManager.Stub.asInterface(b);
+ return sService;
+ }
+
+ /*package*/ NotificationManager(Context context, Handler handler)
+ {
+ mContext = context;
+ }
+
+ /** {@hide} */
+ public static NotificationManager from(Context context) {
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ /**
+ * Post a notification to be shown in the status bar. If a notification with
+ * the same id has already been posted by your application and has not yet been canceled, it
+ * will be replaced by the updated information.
+ *
+ * @param id An identifier for this notification unique within your
+ * application.
+ * @param notification A {@link Notification} object describing what to show the user. Must not
+ * be null.
+ */
+ public void notify(int id, Notification notification)
+ {
+ notify(null, id, notification);
+ }
+
+ /**
+ * Post a notification to be shown in the status bar. If a notification with
+ * the same tag and id has already been posted by your application and has not yet been
+ * canceled, it will be replaced by the updated information.
+ *
+ * @param tag A string identifier for this notification. May be {@code null}.
+ * @param id An identifier for this notification. The pair (tag, id) must be unique
+ * within your application.
+ * @param notification A {@link Notification} object describing what to
+ * show the user. Must not be null.
+ */
+ public void notify(String tag, int id, Notification notification)
+ {
+ notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
+ }
+
+ /**
+ * @hide
+ */
+ public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
+ {
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ // Fix the notification as best we can.
+ Notification.addFieldsFromContext(mContext, notification);
+ if (notification.sound != null) {
+ notification.sound = notification.sound.getCanonicalUri();
+ if (StrictMode.vmFileUriExposureEnabled()) {
+ notification.sound.checkFileUriExposed("Notification.sound");
+ }
+ }
+ fixLegacySmallIcon(notification, pkg);
+ if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ if (notification.getSmallIcon() == null) {
+ throw new IllegalArgumentException("Invalid notification (no valid small icon): "
+ + notification);
+ }
+ }
+ if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
+ notification.reduceImageSizes(mContext);
+ ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ boolean isLowRam = am.isLowRamDevice();
+ final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam);
+ try {
+ service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
+ copy, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void fixLegacySmallIcon(Notification n, String pkg) {
+ if (n.getSmallIcon() == null && n.icon != 0) {
+ n.setSmallIcon(Icon.createWithResource(pkg, n.icon));
+ }
+ }
+
+ /**
+ * Cancel a previously shown notification. If it's transient, the view
+ * will be hidden. If it's persistent, it will be removed from the status
+ * bar.
+ */
+ public void cancel(int id)
+ {
+ cancel(null, id);
+ }
+
+ /**
+ * Cancel a previously shown notification. If it's transient, the view
+ * will be hidden. If it's persistent, it will be removed from the status
+ * bar.
+ */
+ public void cancel(String tag, int id)
+ {
+ cancelAsUser(tag, id, new UserHandle(UserHandle.myUserId()));
+ }
+
+ /**
+ * @hide
+ */
+ public void cancelAsUser(String tag, int id, UserHandle user)
+ {
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
+ try {
+ service.cancelNotificationWithTag(pkg, tag, id, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Cancel all previously shown notifications. See {@link #cancel} for the
+ * detailed behavior.
+ */
+ public void cancelAll()
+ {
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ if (localLOGV) Log.v(TAG, pkg + ": cancelAll()");
+ try {
+ service.cancelAllNotifications(pkg, UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates a group container for {@link NotificationChannel} objects.
+ *
+ * This can be used to rename an existing group.
+ * <p>
+ * Group information is only used for presentation, not for behavior. Groups are optional
+ * for channels, and you can have a mix of channels that belong to groups and channels
+ * that do not.
+ * </p>
+ * <p>
+ * For example, if your application supports multiple accounts, and those accounts will
+ * have similar channels, you can create a group for each account with account specific
+ * labels instead of appending account information to each channel's label.
+ * </p>
+ *
+ * @param group The group to create
+ */
+ public void createNotificationChannelGroup(@NonNull NotificationChannelGroup group) {
+ createNotificationChannelGroups(Arrays.asList(group));
+ }
+
+ /**
+ * Creates multiple notification channel groups.
+ *
+ * @param groups The list of groups to create
+ */
+ public void createNotificationChannelGroups(@NonNull List<NotificationChannelGroup> groups) {
+ INotificationManager service = getService();
+ try {
+ service.createNotificationChannelGroups(mContext.getPackageName(),
+ new ParceledListSlice(groups));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates a notification channel that notifications can be posted to.
+ *
+ * This can also be used to restore a deleted channel and to update an existing channel's
+ * name, description, group, and/or importance.
+ *
+ * <p>The name and description should only be changed if the locale changes
+ * or in response to the user renaming this channel. For example, if a user has a channel
+ * named 'John Doe' that represents messages from a 'John Doe', and 'John Doe' changes his name
+ * to 'John Smith,' the channel can be renamed to match.
+ *
+ * <p>The importance of an existing channel will only be changed if the new importance is lower
+ * than the current value and the user has not altered any settings on this channel.
+ *
+ * <p>The group an existing channel will only be changed if the channel does not already
+ * belong to a group.
+ *
+ * All other fields are ignored for channels that already exist.
+ *
+ * @param channel the channel to create. Note that the created channel may differ from this
+ * value. If the provided channel is malformed, a RemoteException will be
+ * thrown.
+ */
+ public void createNotificationChannel(@NonNull NotificationChannel channel) {
+ createNotificationChannels(Arrays.asList(channel));
+ }
+
+ /**
+ * Creates multiple notification channels that different notifications can be posted to. See
+ * {@link #createNotificationChannel(NotificationChannel)}.
+ *
+ * @param channels the list of channels to attempt to create.
+ */
+ public void createNotificationChannels(@NonNull List<NotificationChannel> channels) {
+ INotificationManager service = getService();
+ try {
+ service.createNotificationChannels(mContext.getPackageName(),
+ new ParceledListSlice(channels));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the notification channel settings for a given channel id.
+ *
+ * The channel must belong to your package, or it will not be returned.
+ */
+ public NotificationChannel getNotificationChannel(String channelId) {
+ INotificationManager service = getService();
+ try {
+ return service.getNotificationChannel(mContext.getPackageName(), channelId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channels belonging to the calling package.
+ */
+ public List<NotificationChannel> getNotificationChannels() {
+ INotificationManager service = getService();
+ try {
+ return service.getNotificationChannels(mContext.getPackageName()).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes the given notification channel.
+ *
+ * <p>If you {@link #createNotificationChannel(NotificationChannel) create} a new channel with
+ * this same id, the deleted channel will be un-deleted with all of the same settings it
+ * had before it was deleted.
+ */
+ public void deleteNotificationChannel(String channelId) {
+ INotificationManager service = getService();
+ try {
+ service.deleteNotificationChannel(mContext.getPackageName(), channelId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channel groups belonging to the calling app.
+ */
+ public List<NotificationChannelGroup> getNotificationChannelGroups() {
+ INotificationManager service = getService();
+ try {
+ return service.getNotificationChannelGroups(mContext.getPackageName()).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes the given notification channel group, and all notification channels that
+ * belong to it.
+ */
+ public void deleteNotificationChannelGroup(String groupId) {
+ INotificationManager service = getService();
+ try {
+ service.deleteNotificationChannelGroup(mContext.getPackageName(), groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public ComponentName getEffectsSuppressor() {
+ INotificationManager service = getService();
+ try {
+ return service.getEffectsSuppressor();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean matchesCallFilter(Bundle extras) {
+ INotificationManager service = getService();
+ try {
+ return service.matchesCallFilter(extras);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isSystemConditionProviderEnabled(String path) {
+ INotificationManager service = getService();
+ try {
+ return service.isSystemConditionProviderEnabled(path);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void setZenMode(int mode, Uri conditionId, String reason) {
+ INotificationManager service = getService();
+ try {
+ service.setZenMode(mode, conditionId, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public int getZenMode() {
+ INotificationManager service = getService();
+ try {
+ return service.getZenMode();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public ZenModeConfig getZenModeConfig() {
+ INotificationManager service = getService();
+ try {
+ return service.getZenModeConfig();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public int getRuleInstanceCount(ComponentName owner) {
+ INotificationManager service = getService();
+ try {
+ return service.getRuleInstanceCount(owner);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns AutomaticZenRules owned by the caller.
+ *
+ * <p>
+ * Throws a SecurityException if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ */
+ public Map<String, AutomaticZenRule> getAutomaticZenRules() {
+ INotificationManager service = getService();
+ try {
+ List<ZenModeConfig.ZenRule> rules = service.getZenRules();
+ Map<String, AutomaticZenRule> ruleMap = new HashMap<>();
+ for (ZenModeConfig.ZenRule rule : rules) {
+ ruleMap.put(rule.id, new AutomaticZenRule(rule.name, rule.component,
+ rule.conditionId, zenModeToInterruptionFilter(rule.zenMode), rule.enabled,
+ rule.creationTime));
+ }
+ return ruleMap;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the AutomaticZenRule with the given id, if it exists and the caller has access.
+ *
+ * <p>
+ * Throws a SecurityException if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ *
+ * <p>
+ * Returns null if there are no zen rules that match the given id, or if the calling package
+ * doesn't own the matching rule. See {@link AutomaticZenRule#getOwner}.
+ */
+ public AutomaticZenRule getAutomaticZenRule(String id) {
+ INotificationManager service = getService();
+ try {
+ return service.getAutomaticZenRule(id);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates the given zen rule.
+ *
+ * <p>
+ * Throws a SecurityException if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ *
+ * @param automaticZenRule the rule to create.
+ * @return The id of the newly created rule; null if the rule could not be created.
+ */
+ public String addAutomaticZenRule(AutomaticZenRule automaticZenRule) {
+ INotificationManager service = getService();
+ try {
+ return service.addAutomaticZenRule(automaticZenRule);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Updates the given zen rule.
+ *
+ * <p>
+ * Throws a SecurityException if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ *
+ * <p>
+ * Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}.
+ * @param id The id of the rule to update
+ * @param automaticZenRule the rule to update.
+ * @return Whether the rule was successfully updated.
+ */
+ public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
+ INotificationManager service = getService();
+ try {
+ return service.updateAutomaticZenRule(id, automaticZenRule);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes the automatic zen rule with the given id.
+ *
+ * <p>
+ * Throws a SecurityException if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ *
+ * <p>
+ * Callers can only delete rules that they own. See {@link AutomaticZenRule#getOwner}.
+ * @param id the id of the rule to delete.
+ * @return Whether the rule was successfully deleted.
+ */
+ public boolean removeAutomaticZenRule(String id) {
+ INotificationManager service = getService();
+ try {
+ return service.removeAutomaticZenRule(id);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes all automatic zen rules owned by the given package.
+ *
+ * @hide
+ */
+ public boolean removeAutomaticZenRules(String packageName) {
+ INotificationManager service = getService();
+ try {
+ return service.removeAutomaticZenRules(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the user specified importance for notifications from the calling
+ * package.
+ */
+ public @Importance int getImportance() {
+ INotificationManager service = getService();
+ try {
+ return service.getPackageImportance(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether notifications from the calling package are blocked.
+ */
+ public boolean areNotificationsEnabled() {
+ INotificationManager service = getService();
+ try {
+ return service.areNotificationsEnabled(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks the ability to read/modify notification do not disturb policy for the calling package.
+ *
+ * <p>
+ * Returns true if the calling package can read/modify notification policy.
+ *
+ * <p>
+ * Apps can request policy access by sending the user to the activity that matches the system
+ * intent action {@link android.provider.Settings#ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS}.
+ *
+ * <p>
+ * Use {@link #ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED} to listen for
+ * user grant or denial of this access.
+ */
+ public boolean isNotificationPolicyAccessGranted() {
+ INotificationManager service = getService();
+ try {
+ return service.isNotificationPolicyAccessGranted(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks whether the user has approved a given
+ * {@link android.service.notification.NotificationListenerService}.
+ *
+ * <p>
+ * The listener service must belong to the calling app.
+ *
+ * <p>
+ * Apps can request notification listener access by sending the user to the activity that
+ * matches the system intent action
+ * {@link android.provider.Settings#ACTION_NOTIFICATION_LISTENER_SETTINGS}.
+ */
+ public boolean isNotificationListenerAccessGranted(ComponentName listener) {
+ INotificationManager service = getService();
+ try {
+ return service.isNotificationListenerAccessGranted(listener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isNotificationAssistantAccessGranted(ComponentName assistant) {
+ INotificationManager service = getService();
+ try {
+ return service.isNotificationAssistantAccessGranted(assistant);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public boolean isNotificationPolicyAccessGrantedForPackage(String pkg) {
+ INotificationManager service = getService();
+ try {
+ return service.isNotificationPolicyAccessGrantedForPackage(pkg);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public List<String> getEnabledNotificationListenerPackages() {
+ INotificationManager service = getService();
+ try {
+ return service.getEnabledNotificationListenerPackages();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the current notification policy.
+ *
+ * <p>
+ * Only available if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ */
+ public Policy getNotificationPolicy() {
+ INotificationManager service = getService();
+ try {
+ return service.getNotificationPolicy(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the current notification policy.
+ *
+ * <p>
+ * Only available if policy access is granted to this package.
+ * See {@link #isNotificationPolicyAccessGranted}.
+ *
+ * @param policy The new desired policy.
+ */
+ public void setNotificationPolicy(@NonNull Policy policy) {
+ checkRequired("policy", policy);
+ INotificationManager service = getService();
+ try {
+ service.setNotificationPolicy(mContext.getOpPackageName(), policy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setNotificationPolicyAccessGranted(String pkg, boolean granted) {
+ INotificationManager service = getService();
+ try {
+ service.setNotificationPolicyAccessGranted(pkg, granted);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setNotificationListenerAccessGranted(ComponentName listener, boolean granted) {
+ INotificationManager service = getService();
+ try {
+ service.setNotificationListenerAccessGranted(listener, granted);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void setNotificationListenerAccessGrantedForUser(ComponentName listener, int userId,
+ boolean granted) {
+ INotificationManager service = getService();
+ try {
+ service.setNotificationListenerAccessGrantedForUser(listener, userId, granted);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public List<ComponentName> getEnabledNotificationListeners(int userId) {
+ INotificationManager service = getService();
+ try {
+ return service.getEnabledNotificationListeners(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private Context mContext;
+
+ private static void checkRequired(String name, Object value) {
+ if (value == null) {
+ throw new IllegalArgumentException(name + " is required");
+ }
+ }
+
+ /**
+ * Notification policy configuration. Represents user-preferences for notification
+ * filtering.
+ */
+ public static class Policy implements android.os.Parcelable {
+ /** Reminder notifications are prioritized. */
+ public static final int PRIORITY_CATEGORY_REMINDERS = 1 << 0;
+ /** Event notifications are prioritized. */
+ public static final int PRIORITY_CATEGORY_EVENTS = 1 << 1;
+ /** Message notifications are prioritized. */
+ public static final int PRIORITY_CATEGORY_MESSAGES = 1 << 2;
+ /** Calls are prioritized. */
+ public static final int PRIORITY_CATEGORY_CALLS = 1 << 3;
+ /** Calls from repeat callers are prioritized. */
+ public static final int PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 4;
+
+ private static final int[] ALL_PRIORITY_CATEGORIES = {
+ PRIORITY_CATEGORY_REMINDERS,
+ PRIORITY_CATEGORY_EVENTS,
+ PRIORITY_CATEGORY_MESSAGES,
+ PRIORITY_CATEGORY_CALLS,
+ PRIORITY_CATEGORY_REPEAT_CALLERS,
+ };
+
+ /** Any sender is prioritized. */
+ public static final int PRIORITY_SENDERS_ANY = 0;
+ /** Saved contacts are prioritized. */
+ public static final int PRIORITY_SENDERS_CONTACTS = 1;
+ /** Only starred contacts are prioritized. */
+ public static final int PRIORITY_SENDERS_STARRED = 2;
+
+ /** Notification categories to prioritize. Bitmask of PRIORITY_CATEGORY_* constants. */
+ public final int priorityCategories;
+
+ /** Notification senders to prioritize for calls. One of:
+ * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */
+ public final int priorityCallSenders;
+
+ /** Notification senders to prioritize for messages. One of:
+ * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */
+ public final int priorityMessageSenders;
+
+ /**
+ * @hide
+ */
+ public static final int SUPPRESSED_EFFECTS_UNSET = -1;
+ /**
+ * Whether notifications suppressed by DND should not interrupt visually (e.g. with
+ * notification lights or by turning the screen on) when the screen is off.
+ */
+ public static final int SUPPRESSED_EFFECT_SCREEN_OFF = 1 << 0;
+ /**
+ * Whether notifications suppressed by DND should not interrupt visually when the screen
+ * is on (e.g. by peeking onto the screen).
+ */
+ public static final int SUPPRESSED_EFFECT_SCREEN_ON = 1 << 1;
+
+ private static final int[] ALL_SUPPRESSED_EFFECTS = {
+ SUPPRESSED_EFFECT_SCREEN_OFF,
+ SUPPRESSED_EFFECT_SCREEN_ON,
+ };
+
+ /**
+ * Visual effects to suppress for a notification that is filtered by Do Not Disturb mode.
+ * Bitmask of SUPPRESSED_EFFECT_* constants.
+ */
+ public final int suppressedVisualEffects;
+
+ /**
+ * Constructs a policy for Do Not Disturb priority mode behavior.
+ *
+ * @param priorityCategories bitmask of categories of notifications that can bypass DND.
+ * @param priorityCallSenders which callers can bypass DND.
+ * @param priorityMessageSenders which message senders can bypass DND.
+ */
+ public Policy(int priorityCategories, int priorityCallSenders, int priorityMessageSenders) {
+ this(priorityCategories, priorityCallSenders, priorityMessageSenders,
+ SUPPRESSED_EFFECTS_UNSET);
+ }
+
+ /**
+ * Constructs a policy for Do Not Disturb priority mode behavior.
+ *
+ * @param priorityCategories bitmask of categories of notifications that can bypass DND.
+ * @param priorityCallSenders which callers can bypass DND.
+ * @param priorityMessageSenders which message senders can bypass DND.
+ * @param suppressedVisualEffects which visual interruptions should be suppressed from
+ * notifications that are filtered by DND.
+ */
+ public Policy(int priorityCategories, int priorityCallSenders, int priorityMessageSenders,
+ int suppressedVisualEffects) {
+ this.priorityCategories = priorityCategories;
+ this.priorityCallSenders = priorityCallSenders;
+ this.priorityMessageSenders = priorityMessageSenders;
+ this.suppressedVisualEffects = suppressedVisualEffects;
+ }
+
+ /** @hide */
+ public Policy(Parcel source) {
+ this(source.readInt(), source.readInt(), source.readInt(), source.readInt());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(priorityCategories);
+ dest.writeInt(priorityCallSenders);
+ dest.writeInt(priorityMessageSenders);
+ dest.writeInt(suppressedVisualEffects);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(priorityCategories, priorityCallSenders, priorityMessageSenders,
+ suppressedVisualEffects);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Policy)) return false;
+ if (o == this) return true;
+ final Policy other = (Policy) o;
+ return other.priorityCategories == priorityCategories
+ && other.priorityCallSenders == priorityCallSenders
+ && other.priorityMessageSenders == priorityMessageSenders
+ && other.suppressedVisualEffects == suppressedVisualEffects;
+ }
+
+ @Override
+ public String toString() {
+ return "NotificationManager.Policy["
+ + "priorityCategories=" + priorityCategoriesToString(priorityCategories)
+ + ",priorityCallSenders=" + prioritySendersToString(priorityCallSenders)
+ + ",priorityMessageSenders=" + prioritySendersToString(priorityMessageSenders)
+ + ",suppressedVisualEffects="
+ + suppressedEffectsToString(suppressedVisualEffects)
+ + "]";
+ }
+
+ public static String suppressedEffectsToString(int effects) {
+ if (effects <= 0) return "";
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < ALL_SUPPRESSED_EFFECTS.length; i++) {
+ final int effect = ALL_SUPPRESSED_EFFECTS[i];
+ if ((effects & effect) != 0) {
+ if (sb.length() > 0) sb.append(',');
+ sb.append(effectToString(effect));
+ }
+ effects &= ~effect;
+ }
+ if (effects != 0) {
+ if (sb.length() > 0) sb.append(',');
+ sb.append("UNKNOWN_").append(effects);
+ }
+ return sb.toString();
+ }
+
+ public static String priorityCategoriesToString(int priorityCategories) {
+ if (priorityCategories == 0) return "";
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < ALL_PRIORITY_CATEGORIES.length; i++) {
+ final int priorityCategory = ALL_PRIORITY_CATEGORIES[i];
+ if ((priorityCategories & priorityCategory) != 0) {
+ if (sb.length() > 0) sb.append(',');
+ sb.append(priorityCategoryToString(priorityCategory));
+ }
+ priorityCategories &= ~priorityCategory;
+ }
+ if (priorityCategories != 0) {
+ if (sb.length() > 0) sb.append(',');
+ sb.append("PRIORITY_CATEGORY_UNKNOWN_").append(priorityCategories);
+ }
+ return sb.toString();
+ }
+
+ private static String effectToString(int effect) {
+ switch (effect) {
+ case SUPPRESSED_EFFECT_SCREEN_OFF: return "SUPPRESSED_EFFECT_SCREEN_OFF";
+ case SUPPRESSED_EFFECT_SCREEN_ON: return "SUPPRESSED_EFFECT_SCREEN_ON";
+ case SUPPRESSED_EFFECTS_UNSET: return "SUPPRESSED_EFFECTS_UNSET";
+ default: return "UNKNOWN_" + effect;
+ }
+ }
+
+ private static String priorityCategoryToString(int priorityCategory) {
+ switch (priorityCategory) {
+ case PRIORITY_CATEGORY_REMINDERS: return "PRIORITY_CATEGORY_REMINDERS";
+ case PRIORITY_CATEGORY_EVENTS: return "PRIORITY_CATEGORY_EVENTS";
+ case PRIORITY_CATEGORY_MESSAGES: return "PRIORITY_CATEGORY_MESSAGES";
+ case PRIORITY_CATEGORY_CALLS: return "PRIORITY_CATEGORY_CALLS";
+ case PRIORITY_CATEGORY_REPEAT_CALLERS: return "PRIORITY_CATEGORY_REPEAT_CALLERS";
+ default: return "PRIORITY_CATEGORY_UNKNOWN_" + priorityCategory;
+ }
+ }
+
+ public static String prioritySendersToString(int prioritySenders) {
+ switch (prioritySenders) {
+ case PRIORITY_SENDERS_ANY: return "PRIORITY_SENDERS_ANY";
+ case PRIORITY_SENDERS_CONTACTS: return "PRIORITY_SENDERS_CONTACTS";
+ case PRIORITY_SENDERS_STARRED: return "PRIORITY_SENDERS_STARRED";
+ default: return "PRIORITY_SENDERS_UNKNOWN_" + prioritySenders;
+ }
+ }
+
+ public static final Parcelable.Creator<Policy> CREATOR = new Parcelable.Creator<Policy>() {
+ @Override
+ public Policy createFromParcel(Parcel in) {
+ return new Policy(in);
+ }
+
+ @Override
+ public Policy[] newArray(int size) {
+ return new Policy[size];
+ }
+ };
+ }
+
+ /**
+ * Recover a list of active notifications: ones that have been posted by the calling app that
+ * have not yet been dismissed by the user or {@link #cancel(String, int)}ed by the app.
+ *
+ * Each notification is embedded in a {@link StatusBarNotification} object, including the
+ * original <code>tag</code> and <code>id</code> supplied to
+ * {@link #notify(String, int, Notification) notify()}
+ * (via {@link StatusBarNotification#getTag() getTag()} and
+ * {@link StatusBarNotification#getId() getId()}) as well as a copy of the original
+ * {@link Notification} object (via {@link StatusBarNotification#getNotification()}).
+ *
+ * @return An array of {@link StatusBarNotification}.
+ */
+ public StatusBarNotification[] getActiveNotifications() {
+ final INotificationManager service = getService();
+ final String pkg = mContext.getPackageName();
+ try {
+ final ParceledListSlice<StatusBarNotification> parceledList
+ = service.getAppActiveNotifications(pkg, UserHandle.myUserId());
+ final List<StatusBarNotification> list = parceledList.getList();
+ return list.toArray(new StatusBarNotification[list.size()]);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the current notification interruption filter.
+ * <p>
+ * The interruption filter defines which notifications are allowed to
+ * interrupt the user (e.g. via sound &amp; vibration) and is applied
+ * globally.
+ */
+ public final @InterruptionFilter int getCurrentInterruptionFilter() {
+ final INotificationManager service = getService();
+ try {
+ return zenModeToInterruptionFilter(service.getZenMode());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the current notification interruption filter.
+ * <p>
+ * The interruption filter defines which notifications are allowed to
+ * interrupt the user (e.g. via sound &amp; vibration) and is applied
+ * globally.
+ * <p>
+ * Only available if policy access is granted to this package. See
+ * {@link #isNotificationPolicyAccessGranted}.
+ */
+ public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
+ final INotificationManager service = getService();
+ try {
+ service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public static int zenModeToInterruptionFilter(int zen) {
+ switch (zen) {
+ case Global.ZEN_MODE_OFF: return INTERRUPTION_FILTER_ALL;
+ case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: return INTERRUPTION_FILTER_PRIORITY;
+ case Global.ZEN_MODE_ALARMS: return INTERRUPTION_FILTER_ALARMS;
+ case Global.ZEN_MODE_NO_INTERRUPTIONS: return INTERRUPTION_FILTER_NONE;
+ default: return INTERRUPTION_FILTER_UNKNOWN;
+ }
+ }
+
+ /** @hide */
+ public static int zenModeFromInterruptionFilter(int interruptionFilter, int defValue) {
+ switch (interruptionFilter) {
+ case INTERRUPTION_FILTER_ALL: return Global.ZEN_MODE_OFF;
+ case INTERRUPTION_FILTER_PRIORITY: return Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ case INTERRUPTION_FILTER_ALARMS: return Global.ZEN_MODE_ALARMS;
+ case INTERRUPTION_FILTER_NONE: return Global.ZEN_MODE_NO_INTERRUPTIONS;
+ default: return defValue;
+ }
+ }
+
+}
diff --git a/android/app/OnActivityPausedListener.java b/android/app/OnActivityPausedListener.java
new file mode 100644
index 00000000..50039737
--- /dev/null
+++ b/android/app/OnActivityPausedListener.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app;
+
+/**
+ * A listener that is called when an Activity is paused. Since this is tracked client side
+ * it should not be trusted to represent the exact current state, but can be used as a hint
+ * for cleanup.
+ *
+ * @hide
+ */
+public interface OnActivityPausedListener {
+ /**
+ * Called when the given activity is paused.
+ */
+ public void onPaused(Activity activity);
+}
diff --git a/android/app/PackageDeleteObserver.java b/android/app/PackageDeleteObserver.java
new file mode 100644
index 00000000..9b83ec19
--- /dev/null
+++ b/android/app/PackageDeleteObserver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.content.pm.IPackageDeleteObserver2;
+
+/** {@hide} */
+public class PackageDeleteObserver {
+ private final IPackageDeleteObserver2.Stub mBinder = new IPackageDeleteObserver2.Stub() {
+ @Override
+ public void onUserActionRequired(Intent intent) {
+ PackageDeleteObserver.this.onUserActionRequired(intent);
+ }
+
+ @Override
+ public void onPackageDeleted(String basePackageName, int returnCode, String msg) {
+ PackageDeleteObserver.this.onPackageDeleted(basePackageName, returnCode, msg);
+ }
+ };
+
+ /** {@hide} */
+ public IPackageDeleteObserver2 getBinder() {
+ return mBinder;
+ }
+
+ public void onUserActionRequired(Intent intent) {
+ }
+
+ public void onPackageDeleted(String basePackageName, int returnCode, String msg) {
+ }
+}
diff --git a/android/app/PackageInstallObserver.java b/android/app/PackageInstallObserver.java
new file mode 100644
index 00000000..ff286794
--- /dev/null
+++ b/android/app/PackageInstallObserver.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.content.pm.IPackageInstallObserver2;
+import android.os.Bundle;
+
+/** {@hide} */
+public class PackageInstallObserver {
+ private final IPackageInstallObserver2.Stub mBinder = new IPackageInstallObserver2.Stub() {
+ @Override
+ public void onUserActionRequired(Intent intent) {
+ PackageInstallObserver.this.onUserActionRequired(intent);
+ }
+
+ @Override
+ public void onPackageInstalled(String basePackageName, int returnCode,
+ String msg, Bundle extras) {
+ PackageInstallObserver.this.onPackageInstalled(basePackageName, returnCode, msg,
+ extras);
+ }
+ };
+
+ /** {@hide} */
+ public IPackageInstallObserver2 getBinder() {
+ return mBinder;
+ }
+
+ public void onUserActionRequired(Intent intent) {
+ }
+
+ /**
+ * This method will be called to report the result of the package
+ * installation attempt.
+ *
+ * @param basePackageName Name of the package whose installation was
+ * attempted
+ * @param extras If non-null, this Bundle contains extras providing
+ * additional information about an install failure. See
+ * {@link android.content.pm.PackageManager} for documentation
+ * about which extras apply to various failures; in particular
+ * the strings named EXTRA_FAILURE_*.
+ * @param returnCode The numeric success or failure code indicating the
+ * basic outcome
+ * @hide
+ */
+ public void onPackageInstalled(String basePackageName, int returnCode, String msg,
+ Bundle extras) {
+ }
+}
diff --git a/android/app/PendingIntent.java b/android/app/PendingIntent.java
new file mode 100644
index 00000000..a25c2267
--- /dev/null
+++ b/android/app/PendingIntent.java
@@ -0,0 +1,1162 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.AndroidException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A description of an Intent and target action to perform with it. Instances
+ * of this class are created with {@link #getActivity}, {@link #getActivities},
+ * {@link #getBroadcast}, and {@link #getService}; the returned object can be
+ * handed to other applications so that they can perform the action you
+ * described on your behalf at a later time.
+ *
+ * <p>By giving a PendingIntent to another application,
+ * you are granting it the right to perform the operation you have specified
+ * as if the other application was yourself (with the same permissions and
+ * identity). As such, you should be careful about how you build the PendingIntent:
+ * almost always, for example, the base Intent you supply should have the component
+ * name explicitly set to one of your own components, to ensure it is ultimately
+ * sent there and nowhere else.
+ *
+ * <p>A PendingIntent itself is simply a reference to a token maintained by
+ * the system describing the original data used to retrieve it. This means
+ * that, even if its owning application's process is killed, the
+ * PendingIntent itself will remain usable from other processes that
+ * have been given it. If the creating application later re-retrieves the
+ * same kind of PendingIntent (same operation, same Intent action, data,
+ * categories, and components, and same flags), it will receive a PendingIntent
+ * representing the same token if that is still valid, and can thus call
+ * {@link #cancel} to remove it.
+ *
+ * <p>Because of this behavior, it is important to know when two Intents
+ * are considered to be the same for purposes of retrieving a PendingIntent.
+ * A common mistake people make is to create multiple PendingIntent objects
+ * with Intents that only vary in their "extra" contents, expecting to get
+ * a different PendingIntent each time. This does <em>not</em> happen. The
+ * parts of the Intent that are used for matching are the same ones defined
+ * by {@link Intent#filterEquals(Intent) Intent.filterEquals}. If you use two
+ * Intent objects that are equivalent as per
+ * {@link Intent#filterEquals(Intent) Intent.filterEquals}, then you will get
+ * the same PendingIntent for both of them.
+ *
+ * <p>There are two typical ways to deal with this.
+ *
+ * <p>If you truly need multiple distinct PendingIntent objects active at
+ * the same time (such as to use as two notifications that are both shown
+ * at the same time), then you will need to ensure there is something that
+ * is different about them to associate them with different PendingIntents.
+ * This may be any of the Intent attributes considered by
+ * {@link Intent#filterEquals(Intent) Intent.filterEquals}, or different
+ * request code integers supplied to {@link #getActivity}, {@link #getActivities},
+ * {@link #getBroadcast}, or {@link #getService}.
+ *
+ * <p>If you only need one PendingIntent active at a time for any of the
+ * Intents you will use, then you can alternatively use the flags
+ * {@link #FLAG_CANCEL_CURRENT} or {@link #FLAG_UPDATE_CURRENT} to either
+ * cancel or modify whatever current PendingIntent is associated with the
+ * Intent you are supplying.
+ */
+public final class PendingIntent implements Parcelable {
+ private final IIntentSender mTarget;
+ private IBinder mWhitelistToken;
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ FLAG_ONE_SHOT,
+ FLAG_NO_CREATE,
+ FLAG_CANCEL_CURRENT,
+ FLAG_UPDATE_CURRENT,
+ FLAG_IMMUTABLE,
+
+ Intent.FILL_IN_ACTION,
+ Intent.FILL_IN_DATA,
+ Intent.FILL_IN_CATEGORIES,
+ Intent.FILL_IN_COMPONENT,
+ Intent.FILL_IN_PACKAGE,
+ Intent.FILL_IN_SOURCE_BOUNDS,
+ Intent.FILL_IN_SELECTOR,
+ Intent.FILL_IN_CLIP_DATA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Flags {}
+
+ /**
+ * Flag indicating that this PendingIntent can be used only once.
+ * For use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}. <p>If set, after
+ * {@link #send()} is called on it, it will be automatically
+ * canceled for you and any future attempt to send through it will fail.
+ */
+ public static final int FLAG_ONE_SHOT = 1<<30;
+ /**
+ * Flag indicating that if the described PendingIntent does not
+ * already exist, then simply return null instead of creating it.
+ * For use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}.
+ */
+ public static final int FLAG_NO_CREATE = 1<<29;
+ /**
+ * Flag indicating that if the described PendingIntent already exists,
+ * the current one should be canceled before generating a new one.
+ * For use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}. <p>You can use
+ * this to retrieve a new PendingIntent when you are only changing the
+ * extra data in the Intent; by canceling the previous pending intent,
+ * this ensures that only entities given the new data will be able to
+ * launch it. If this assurance is not an issue, consider
+ * {@link #FLAG_UPDATE_CURRENT}.
+ */
+ public static final int FLAG_CANCEL_CURRENT = 1<<28;
+ /**
+ * Flag indicating that if the described PendingIntent already exists,
+ * then keep it but replace its extra data with what is in this new
+ * Intent. For use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}. <p>This can be used if you are creating intents where only the
+ * extras change, and don't care that any entities that received your
+ * previous PendingIntent will be able to launch it with your new
+ * extras even if they are not explicitly given to it.
+ */
+ public static final int FLAG_UPDATE_CURRENT = 1<<27;
+
+ /**
+ * Flag indicating that the created PendingIntent should be immutable.
+ * This means that the additional intent argument passed to the send
+ * methods to fill in unpopulated properties of this intent will be
+ * ignored.
+ */
+ public static final int FLAG_IMMUTABLE = 1<<26;
+
+ /**
+ * Exception thrown when trying to send through a PendingIntent that
+ * has been canceled or is otherwise no longer able to execute the request.
+ */
+ public static class CanceledException extends AndroidException {
+ public CanceledException() {
+ }
+
+ public CanceledException(String name) {
+ super(name);
+ }
+
+ public CanceledException(Exception cause) {
+ super(cause);
+ }
+ }
+
+ /**
+ * Callback interface for discovering when a send operation has
+ * completed. Primarily for use with a PendingIntent that is
+ * performing a broadcast, this provides the same information as
+ * calling {@link Context#sendOrderedBroadcast(Intent, String,
+ * android.content.BroadcastReceiver, Handler, int, String, Bundle)
+ * Context.sendBroadcast()} with a final BroadcastReceiver.
+ */
+ public interface OnFinished {
+ /**
+ * Called when a send operation as completed.
+ *
+ * @param pendingIntent The PendingIntent this operation was sent through.
+ * @param intent The original Intent that was sent.
+ * @param resultCode The final result code determined by the send.
+ * @param resultData The final data collected by a broadcast.
+ * @param resultExtras The final extras collected by a broadcast.
+ */
+ void onSendFinished(PendingIntent pendingIntent, Intent intent,
+ int resultCode, String resultData, Bundle resultExtras);
+ }
+
+ private static class FinishedDispatcher extends IIntentReceiver.Stub
+ implements Runnable {
+ private final PendingIntent mPendingIntent;
+ private final OnFinished mWho;
+ private final Handler mHandler;
+ private Intent mIntent;
+ private int mResultCode;
+ private String mResultData;
+ private Bundle mResultExtras;
+ private static Handler sDefaultSystemHandler;
+ FinishedDispatcher(PendingIntent pi, OnFinished who, Handler handler) {
+ mPendingIntent = pi;
+ mWho = who;
+ if (handler == null && ActivityThread.isSystem()) {
+ // We assign a default handler for the system process to avoid deadlocks when
+ // processing receivers in various components that hold global service locks.
+ if (sDefaultSystemHandler == null) {
+ sDefaultSystemHandler = new Handler(Looper.getMainLooper());
+ }
+ mHandler = sDefaultSystemHandler;
+ } else {
+ mHandler = handler;
+ }
+ }
+ public void performReceive(Intent intent, int resultCode, String data,
+ Bundle extras, boolean serialized, boolean sticky, int sendingUser) {
+ mIntent = intent;
+ mResultCode = resultCode;
+ mResultData = data;
+ mResultExtras = extras;
+ if (mHandler == null) {
+ run();
+ } else {
+ mHandler.post(this);
+ }
+ }
+ public void run() {
+ mWho.onSendFinished(mPendingIntent, mIntent, mResultCode,
+ mResultData, mResultExtras);
+ }
+ }
+
+ /**
+ * Listener for observing when pending intents are written to a parcel.
+ *
+ * @hide
+ */
+ public interface OnMarshaledListener {
+ /**
+ * Called when a pending intent is written to a parcel.
+ *
+ * @param intent The pending intent.
+ * @param parcel The parcel to which it was written.
+ * @param flags The parcel flags when it was written.
+ */
+ void onMarshaled(PendingIntent intent, Parcel parcel, int flags);
+ }
+
+ private static final ThreadLocal<OnMarshaledListener> sOnMarshaledListener
+ = new ThreadLocal<>();
+
+ /**
+ * Registers an listener for pending intents being written to a parcel.
+ *
+ * @param listener The listener, null to clear.
+ *
+ * @hide
+ */
+ public static void setOnMarshaledListener(OnMarshaledListener listener) {
+ sOnMarshaledListener.set(listener);
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a new activity, like calling
+ * {@link Context#startActivity(Intent) Context.startActivity(Intent)}.
+ * Note that the activity will be started outside of the context of an
+ * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent.
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent}
+ * you supply here should almost always be an <em>explicit intent</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the activity.
+ * @param requestCode Private request code for the sender
+ * @param intent Intent of the activity to be launched.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getActivity(Context context, int requestCode,
+ Intent intent, @Flags int flags) {
+ return getActivity(context, requestCode, intent, flags, null);
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a new activity, like calling
+ * {@link Context#startActivity(Intent) Context.startActivity(Intent)}.
+ * Note that the activity will be started outside of the context of an
+ * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent.
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent}
+ * you supply here should almost always be an <em>explicit intent</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the activity.
+ * @param requestCode Private request code for the sender
+ * @param intent Intent of the activity to be launched.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ * @param options Additional options for how the Activity should be started.
+ * May be null if there are no options.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getActivity(Context context, int requestCode,
+ @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(context);
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
+ null, null, requestCode, new Intent[] { intent },
+ resolvedType != null ? new String[] { resolvedType } : null,
+ flags, options, UserHandle.myUserId());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Note that UserHandle.CURRENT will be interpreted at the time the
+ * activity is started, not when the pending intent is created.
+ */
+ public static PendingIntent getActivityAsUser(Context context, int requestCode,
+ @NonNull Intent intent, int flags, Bundle options, UserHandle user) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(context);
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
+ null, null, requestCode, new Intent[] { intent },
+ resolvedType != null ? new String[] { resolvedType } : null,
+ flags, options, user.getIdentifier());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Like {@link #getActivity(Context, int, Intent, int)}, but allows an
+ * array of Intents to be supplied. The last Intent in the array is
+ * taken as the primary key for the PendingIntent, like the single Intent
+ * given to {@link #getActivity(Context, int, Intent, int)}. Upon sending
+ * the resulting PendingIntent, all of the Intents are started in the same
+ * way as they would be by passing them to {@link Context#startActivities(Intent[])}.
+ *
+ * <p class="note">
+ * The <em>first</em> intent in the array will be started outside of the context of an
+ * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent. (Activities after
+ * the first in the array are started in the context of the previous activity
+ * in the array, so FLAG_ACTIVITY_NEW_TASK is not needed nor desired for them.)
+ * </p>
+ *
+ * <p class="note">
+ * The <em>last</em> intent in the array represents the key for the
+ * PendingIntent. In other words, it is the significant element for matching
+ * (as done with the single intent given to {@link #getActivity(Context, int, Intent, int)},
+ * its content will be the subject of replacement by
+ * {@link #send(Context, int, Intent)} and {@link #FLAG_UPDATE_CURRENT}, etc.
+ * This is because it is the most specific of the supplied intents, and the
+ * UI the user actually sees when the intents are started.
+ * </p>
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent} objects
+ * you supply here should almost always be <em>explicit intents</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the activity.
+ * @param requestCode Private request code for the sender
+ * @param intents Array of Intents of the activities to be launched.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getActivities(Context context, int requestCode,
+ @NonNull Intent[] intents, @Flags int flags) {
+ return getActivities(context, requestCode, intents, flags, null);
+ }
+
+ /**
+ * Like {@link #getActivity(Context, int, Intent, int)}, but allows an
+ * array of Intents to be supplied. The last Intent in the array is
+ * taken as the primary key for the PendingIntent, like the single Intent
+ * given to {@link #getActivity(Context, int, Intent, int)}. Upon sending
+ * the resulting PendingIntent, all of the Intents are started in the same
+ * way as they would be by passing them to {@link Context#startActivities(Intent[])}.
+ *
+ * <p class="note">
+ * The <em>first</em> intent in the array will be started outside of the context of an
+ * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent. (Activities after
+ * the first in the array are started in the context of the previous activity
+ * in the array, so FLAG_ACTIVITY_NEW_TASK is not needed nor desired for them.)
+ * </p>
+ *
+ * <p class="note">
+ * The <em>last</em> intent in the array represents the key for the
+ * PendingIntent. In other words, it is the significant element for matching
+ * (as done with the single intent given to {@link #getActivity(Context, int, Intent, int)},
+ * its content will be the subject of replacement by
+ * {@link #send(Context, int, Intent)} and {@link #FLAG_UPDATE_CURRENT}, etc.
+ * This is because it is the most specific of the supplied intents, and the
+ * UI the user actually sees when the intents are started.
+ * </p>
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent} objects
+ * you supply here should almost always be <em>explicit intents</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the activity.
+ * @param requestCode Private request code for the sender
+ * @param intents Array of Intents of the activities to be launched.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * {@link #FLAG_IMMUTABLE} or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getActivities(Context context, int requestCode,
+ @NonNull Intent[] intents, @Flags int flags, @Nullable Bundle options) {
+ String packageName = context.getPackageName();
+ String[] resolvedTypes = new String[intents.length];
+ for (int i=0; i<intents.length; i++) {
+ intents[i].migrateExtraStreamToClipData();
+ intents[i].prepareToLeaveProcess(context);
+ resolvedTypes[i] = intents[i].resolveTypeIfNeeded(context.getContentResolver());
+ }
+ try {
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
+ null, null, requestCode, intents, resolvedTypes, flags, options,
+ UserHandle.myUserId());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Note that UserHandle.CURRENT will be interpreted at the time the
+ * activity is started, not when the pending intent is created.
+ */
+ public static PendingIntent getActivitiesAsUser(Context context, int requestCode,
+ @NonNull Intent[] intents, int flags, Bundle options, UserHandle user) {
+ String packageName = context.getPackageName();
+ String[] resolvedTypes = new String[intents.length];
+ for (int i=0; i<intents.length; i++) {
+ intents[i].migrateExtraStreamToClipData();
+ intents[i].prepareToLeaveProcess(context);
+ resolvedTypes[i] = intents[i].resolveTypeIfNeeded(context.getContentResolver());
+ }
+ try {
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
+ null, null, requestCode, intents, resolvedTypes,
+ flags, options, user.getIdentifier());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve a PendingIntent that will perform a broadcast, like calling
+ * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}.
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent}
+ * you supply here should almost always be an <em>explicit intent</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should perform
+ * the broadcast.
+ * @param requestCode Private request code for the sender
+ * @param intent The Intent to be broadcast.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * {@link #FLAG_IMMUTABLE} or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getBroadcast(Context context, int requestCode,
+ Intent intent, @Flags int flags) {
+ return getBroadcastAsUser(context, requestCode, intent, flags,
+ new UserHandle(UserHandle.myUserId()));
+ }
+
+ /**
+ * @hide
+ * Note that UserHandle.CURRENT will be interpreted at the time the
+ * broadcast is sent, not when the pending intent is created.
+ */
+ public static PendingIntent getBroadcastAsUser(Context context, int requestCode,
+ Intent intent, int flags, UserHandle userHandle) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ intent.prepareToLeaveProcess(context);
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ ActivityManager.INTENT_SENDER_BROADCAST, packageName,
+ null, null, requestCode, new Intent[] { intent },
+ resolvedType != null ? new String[] { resolvedType } : null,
+ flags, null, userHandle.getIdentifier());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a service, like calling
+ * {@link Context#startService Context.startService()}. The start
+ * arguments given to the service will come from the extras of the Intent.
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent}
+ * you supply here should almost always be an <em>explicit intent</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the service.
+ * @param requestCode Private request code for the sender
+ * @param intent An Intent describing the service to be started.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * {@link #FLAG_IMMUTABLE} or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getService(Context context, int requestCode,
+ @NonNull Intent intent, @Flags int flags) {
+ return buildServicePendingIntent(context, requestCode, intent, flags,
+ ActivityManager.INTENT_SENDER_SERVICE);
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a foreground service, like calling
+ * {@link Context#startForegroundService Context.startForegroundService()}. The start
+ * arguments given to the service will come from the extras of the Intent.
+ *
+ * <p class="note">For security reasons, the {@link android.content.Intent}
+ * you supply here should almost always be an <em>explicit intent</em>,
+ * that is specify an explicit component to be delivered to through
+ * {@link Intent#setClass(android.content.Context, Class) Intent.setClass}</p>
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the service.
+ * @param requestCode Private request code for the sender
+ * @param intent An Intent describing the service to be started.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT},
+ * {@link #FLAG_IMMUTABLE} or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getForegroundService(Context context, int requestCode,
+ @NonNull Intent intent, @Flags int flags) {
+ return buildServicePendingIntent(context, requestCode, intent, flags,
+ ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE);
+ }
+
+ private static PendingIntent buildServicePendingIntent(Context context, int requestCode,
+ Intent intent, int flags, int serviceKind) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ intent.prepareToLeaveProcess(context);
+ IIntentSender target =
+ ActivityManager.getService().getIntentSender(
+ serviceKind, packageName,
+ null, null, requestCode, new Intent[] { intent },
+ resolvedType != null ? new String[] { resolvedType } : null,
+ flags, null, UserHandle.myUserId());
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve a IntentSender object that wraps the existing sender of the PendingIntent
+ *
+ * @return Returns a IntentSender object that wraps the sender of PendingIntent
+ *
+ */
+ public IntentSender getIntentSender() {
+ return new IntentSender(mTarget, mWhitelistToken);
+ }
+
+ /**
+ * Cancel a currently active PendingIntent. Only the original application
+ * owning a PendingIntent can cancel it.
+ */
+ public void cancel() {
+ try {
+ ActivityManager.getService().cancelIntentSender(mTarget);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send() throws CanceledException {
+ send(null, 0, null, null, null, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent.
+ *
+ * @param code Result code to supply back to the PendingIntent's target.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(int code) throws CanceledException {
+ send(null, code, null, null, null, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use.
+ *
+ * @param context The Context of the caller.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. If flag {@link #FLAG_IMMUTABLE} was set when this
+ * pending intent was created, this argument will be ignored.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, @Nullable Intent intent)
+ throws CanceledException {
+ send(context, code, intent, null, null, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to be notified when the send has completed.
+ *
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(int code, @Nullable OnFinished onFinished, @Nullable Handler handler)
+ throws CanceledException {
+ send(null, code, null, onFinished, handler, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use and be notified
+ * when the send has completed.
+ *
+ * <p>For the intent parameter, a PendingIntent
+ * often has restrictions on which fields can be supplied here, based on
+ * how the PendingIntent was retrieved in {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService}.
+ *
+ * @param context The Context of the caller. This may be null if
+ * <var>intent</var> is also null.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. Use null to not modify the original Intent.
+ * If flag {@link #FLAG_IMMUTABLE} was set when this pending intent was
+ * created, this argument will be ignored.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ *
+ * @see #send()
+ * @see #send(int)
+ * @see #send(Context, int, Intent)
+ * @see #send(int, android.app.PendingIntent.OnFinished, Handler)
+ * @see #send(Context, int, Intent, OnFinished, Handler, String)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, @Nullable Intent intent,
+ @Nullable OnFinished onFinished, @Nullable Handler handler) throws CanceledException {
+ send(context, code, intent, onFinished, handler, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use and be notified
+ * when the send has completed.
+ *
+ * <p>For the intent parameter, a PendingIntent
+ * often has restrictions on which fields can be supplied here, based on
+ * how the PendingIntent was retrieved in {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService}.
+ *
+ * @param context The Context of the caller. This may be null if
+ * <var>intent</var> is also null.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. Use null to not modify the original Intent.
+ * If flag {@link #FLAG_IMMUTABLE} was set when this pending intent was
+ * created, this argument will be ignored.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ * @param requiredPermission Name of permission that a recipient of the PendingIntent
+ * is required to hold. This is only valid for broadcast intents, and
+ * corresponds to the permission argument in
+ * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}.
+ * If null, no permission is required.
+ *
+ * @see #send()
+ * @see #send(int)
+ * @see #send(Context, int, Intent)
+ * @see #send(int, android.app.PendingIntent.OnFinished, Handler)
+ * @see #send(Context, int, Intent, OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, @Nullable Intent intent,
+ @Nullable OnFinished onFinished, @Nullable Handler handler,
+ @Nullable String requiredPermission)
+ throws CanceledException {
+ send(context, code, intent, onFinished, handler, requiredPermission, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use and be notified
+ * when the send has completed.
+ *
+ * <p>For the intent parameter, a PendingIntent
+ * often has restrictions on which fields can be supplied here, based on
+ * how the PendingIntent was retrieved in {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService}.
+ *
+ * @param context The Context of the caller. This may be null if
+ * <var>intent</var> is also null.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. Use null to not modify the original Intent.
+ * If flag {@link #FLAG_IMMUTABLE} was set when this pending intent was
+ * created, this argument will be ignored.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ * @param requiredPermission Name of permission that a recipient of the PendingIntent
+ * is required to hold. This is only valid for broadcast intents, and
+ * corresponds to the permission argument in
+ * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}.
+ * If null, no permission is required.
+ * @param options Additional options the caller would like to provide to modify the sending
+ * behavior. May be built from an {@link ActivityOptions} to apply to an activity start.
+ *
+ * @see #send()
+ * @see #send(int)
+ * @see #send(Context, int, Intent)
+ * @see #send(int, android.app.PendingIntent.OnFinished, Handler)
+ * @see #send(Context, int, Intent, OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, @Nullable Intent intent,
+ @Nullable OnFinished onFinished, @Nullable Handler handler,
+ @Nullable String requiredPermission, @Nullable Bundle options)
+ throws CanceledException {
+ try {
+ String resolvedType = intent != null ?
+ intent.resolveTypeIfNeeded(context.getContentResolver())
+ : null;
+ int res = ActivityManager.getService().sendIntentSender(
+ mTarget, mWhitelistToken, code, intent, resolvedType,
+ onFinished != null
+ ? new FinishedDispatcher(this, onFinished, handler)
+ : null,
+ requiredPermission, options);
+ if (res < 0) {
+ throw new CanceledException();
+ }
+ } catch (RemoteException e) {
+ throw new CanceledException(e);
+ }
+ }
+
+ /**
+ * @deprecated Renamed to {@link #getCreatorPackage()}.
+ */
+ @Deprecated
+ public String getTargetPackage() {
+ try {
+ return ActivityManager.getService()
+ .getPackageForIntentSender(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the package name of the application that created this
+ * PendingIntent, that is the identity under which you will actually be
+ * sending the Intent. The returned string is supplied by the system, so
+ * that an application can not spoof its package.
+ *
+ * <p class="note">Be careful about how you use this. All this tells you is
+ * who created the PendingIntent. It does <strong>not</strong> tell you who
+ * handed the PendingIntent to you: that is, PendingIntent objects are intended to be
+ * passed between applications, so the PendingIntent you receive from an application
+ * could actually be one it received from another application, meaning the result
+ * you get here will identify the original application. Because of this, you should
+ * only use this information to identify who you expect to be interacting with
+ * through a {@link #send} call, not who gave you the PendingIntent.</p>
+ *
+ * @return The package name of the PendingIntent, or null if there is
+ * none associated with it.
+ */
+ @Nullable
+ public String getCreatorPackage() {
+ try {
+ return ActivityManager.getService()
+ .getPackageForIntentSender(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the uid of the application that created this
+ * PendingIntent, that is the identity under which you will actually be
+ * sending the Intent. The returned integer is supplied by the system, so
+ * that an application can not spoof its uid.
+ *
+ * <p class="note">Be careful about how you use this. All this tells you is
+ * who created the PendingIntent. It does <strong>not</strong> tell you who
+ * handed the PendingIntent to you: that is, PendingIntent objects are intended to be
+ * passed between applications, so the PendingIntent you receive from an application
+ * could actually be one it received from another application, meaning the result
+ * you get here will identify the original application. Because of this, you should
+ * only use this information to identify who you expect to be interacting with
+ * through a {@link #send} call, not who gave you the PendingIntent.</p>
+ *
+ * @return The uid of the PendingIntent, or -1 if there is
+ * none associated with it.
+ */
+ public int getCreatorUid() {
+ try {
+ return ActivityManager.getService()
+ .getUidForIntentSender(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the user handle of the application that created this
+ * PendingIntent, that is the user under which you will actually be
+ * sending the Intent. The returned UserHandle is supplied by the system, so
+ * that an application can not spoof its user. See
+ * {@link android.os.Process#myUserHandle() Process.myUserHandle()} for
+ * more explanation of user handles.
+ *
+ * <p class="note">Be careful about how you use this. All this tells you is
+ * who created the PendingIntent. It does <strong>not</strong> tell you who
+ * handed the PendingIntent to you: that is, PendingIntent objects are intended to be
+ * passed between applications, so the PendingIntent you receive from an application
+ * could actually be one it received from another application, meaning the result
+ * you get here will identify the original application. Because of this, you should
+ * only use this information to identify who you expect to be interacting with
+ * through a {@link #send} call, not who gave you the PendingIntent.</p>
+ *
+ * @return The user handle of the PendingIntent, or null if there is
+ * none associated with it.
+ */
+ @Nullable
+ public UserHandle getCreatorUserHandle() {
+ try {
+ int uid = ActivityManager.getService()
+ .getUidForIntentSender(mTarget);
+ return uid > 0 ? new UserHandle(UserHandle.getUserId(uid)) : null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Check to verify that this PendingIntent targets a specific package.
+ */
+ public boolean isTargetedToPackage() {
+ try {
+ return ActivityManager.getService()
+ .isIntentSenderTargetedToPackage(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Check whether this PendingIntent will launch an Activity.
+ */
+ public boolean isActivity() {
+ try {
+ return ActivityManager.getService()
+ .isIntentSenderAnActivity(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Check whether this PendingIntent will launch a foreground service
+ */
+ public boolean isForegroundService() {
+ try {
+ return ActivityManager.getService()
+ .isIntentSenderAForegroundService(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Return the Intent of this PendingIntent.
+ */
+ public Intent getIntent() {
+ try {
+ return ActivityManager.getService()
+ .getIntentForIntentSender(mTarget);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Return descriptive tag for this PendingIntent.
+ */
+ public String getTag(String prefix) {
+ try {
+ return ActivityManager.getService()
+ .getTagForIntentSender(mTarget, prefix);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Comparison operator on two PendingIntent objects, such that true
+ * is returned then they both represent the same operation from the
+ * same package. This allows you to use {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService} multiple times (even
+ * across a process being killed), resulting in different PendingIntent
+ * objects but whose equals() method identifies them as being the same
+ * operation.
+ */
+ @Override
+ public boolean equals(Object otherObj) {
+ if (otherObj instanceof PendingIntent) {
+ return mTarget.asBinder().equals(((PendingIntent)otherObj)
+ .mTarget.asBinder());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mTarget.asBinder().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("PendingIntent{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(": ");
+ sb.append(mTarget != null ? mTarget.asBinder() : null);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStrongBinder(mTarget.asBinder());
+ OnMarshaledListener listener = sOnMarshaledListener.get();
+ if (listener != null) {
+ listener.onMarshaled(this, out, flags);
+ }
+
+ }
+
+ public static final Parcelable.Creator<PendingIntent> CREATOR
+ = new Parcelable.Creator<PendingIntent>() {
+ public PendingIntent createFromParcel(Parcel in) {
+ IBinder target = in.readStrongBinder();
+ return target != null
+ ? new PendingIntent(target, in.getClassCookie(PendingIntent.class))
+ : null;
+ }
+
+ public PendingIntent[] newArray(int size) {
+ return new PendingIntent[size];
+ }
+ };
+
+ /**
+ * Convenience function for writing either a PendingIntent or null pointer to
+ * a Parcel. You must use this with {@link #readPendingIntentOrNullFromParcel}
+ * for later reading it.
+ *
+ * @param sender The PendingIntent to write, or null.
+ * @param out Where to write the PendingIntent.
+ */
+ public static void writePendingIntentOrNullToParcel(@Nullable PendingIntent sender,
+ @NonNull Parcel out) {
+ out.writeStrongBinder(sender != null ? sender.mTarget.asBinder()
+ : null);
+ }
+
+ /**
+ * Convenience function for reading either a PendingIntent or null pointer from
+ * a Parcel. You must have previously written the PendingIntent with
+ * {@link #writePendingIntentOrNullToParcel}.
+ *
+ * @param in The Parcel containing the written PendingIntent.
+ *
+ * @return Returns the PendingIntent read from the Parcel, or null if null had
+ * been written.
+ */
+ @Nullable
+ public static PendingIntent readPendingIntentOrNullFromParcel(@NonNull Parcel in) {
+ IBinder b = in.readStrongBinder();
+ return b != null ? new PendingIntent(b, in.getClassCookie(PendingIntent.class)) : null;
+ }
+
+ /*package*/ PendingIntent(IIntentSender target) {
+ mTarget = target;
+ }
+
+ /*package*/ PendingIntent(IBinder target, Object cookie) {
+ mTarget = IIntentSender.Stub.asInterface(target);
+ if (cookie != null) {
+ mWhitelistToken = (IBinder)cookie;
+ }
+ }
+
+ /** @hide */
+ public IIntentSender getTarget() {
+ return mTarget;
+ }
+
+ /** @hide */
+ public IBinder getWhitelistToken() {
+ return mWhitelistToken;
+ }
+}
diff --git a/android/app/PictureInPictureArgs.java b/android/app/PictureInPictureArgs.java
new file mode 100644
index 00000000..cbe8bb9d
--- /dev/null
+++ b/android/app/PictureInPictureArgs.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Rational;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** @removed */
+@Deprecated
+public final class PictureInPictureArgs implements Parcelable {
+
+ /**
+ * Builder class for {@link PictureInPictureArgs} objects.
+ */
+ public static class Builder {
+
+ @Nullable
+ private Rational mAspectRatio;
+
+ @Nullable
+ private List<RemoteAction> mUserActions;
+
+ @Nullable
+ private Rect mSourceRectHint;
+
+ /**
+ * Sets the aspect ratio. This aspect ratio is defined as the desired width / height, and
+ * does not change upon device rotation.
+ *
+ * @param aspectRatio the new aspect ratio for the activity in picture-in-picture, must be
+ * between 2.39:1 and 1:2.39 (inclusive).
+ *
+ * @return this builder instance.
+ */
+ public Builder setAspectRatio(Rational aspectRatio) {
+ mAspectRatio = aspectRatio;
+ return this;
+ }
+
+ /**
+ * Sets the user actions. If there are more than
+ * {@link Activity#getMaxNumPictureInPictureActions()} actions, then the input list
+ * will be truncated to that number.
+ *
+ * @param actions the new actions to show in the picture-in-picture menu.
+ *
+ * @return this builder instance.
+ *
+ * @see RemoteAction
+ */
+ public Builder setActions(List<RemoteAction> actions) {
+ if (mUserActions != null) {
+ mUserActions = null;
+ }
+ if (actions != null) {
+ mUserActions = new ArrayList<>(actions);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the source bounds hint. These bounds are only used when an activity first enters
+ * picture-in-picture, and describe the bounds in window coordinates of activity entering
+ * picture-in-picture that will be visible following the transition. For the best effect,
+ * these bounds should also match the aspect ratio in the arguments.
+ *
+ * @param launchBounds window-coordinate bounds indicating the area of the activity that
+ * will still be visible following the transition into picture-in-picture (eg. the video
+ * view bounds in a video player)
+ *
+ * @return this builder instance.
+ */
+ public Builder setSourceRectHint(Rect launchBounds) {
+ if (launchBounds == null) {
+ mSourceRectHint = null;
+ } else {
+ mSourceRectHint = new Rect(launchBounds);
+ }
+ return this;
+ }
+
+ public PictureInPictureArgs build() {
+ PictureInPictureArgs args = new PictureInPictureArgs(mAspectRatio, mUserActions,
+ mSourceRectHint);
+ return args;
+ }
+ }
+
+ /**
+ * The expected aspect ratio of the picture-in-picture.
+ */
+ @Nullable
+ private Rational mAspectRatio;
+
+ /**
+ * The set of actions that are associated with this activity when in picture-in-picture.
+ */
+ @Nullable
+ private List<RemoteAction> mUserActions;
+
+ /**
+ * The source bounds hint used when entering picture-in-picture, relative to the window bounds.
+ * We can use this internally for the transition into picture-in-picture to ensure that a
+ * particular source rect is visible throughout the whole transition.
+ */
+ @Nullable
+ private Rect mSourceRectHint;
+
+ /**
+ * The content insets that are used with the source hint rect for the transition into PiP where
+ * the insets are removed at the beginning of the transition.
+ */
+ @Nullable
+ private Rect mSourceRectHintInsets;
+
+ /**
+ * @hide
+ */
+ @Deprecated
+ public PictureInPictureArgs() {
+ }
+
+ /**
+ * @hide
+ */
+ @Deprecated
+ public PictureInPictureArgs(float aspectRatio, List<RemoteAction> actions) {
+ setAspectRatio(aspectRatio);
+ setActions(actions);
+ }
+
+ private PictureInPictureArgs(Parcel in) {
+ if (in.readInt() != 0) {
+ mAspectRatio = new Rational(in.readInt(), in.readInt());
+ }
+ if (in.readInt() != 0) {
+ mUserActions = new ArrayList<>();
+ in.readParcelableList(mUserActions, RemoteAction.class.getClassLoader());
+ }
+ if (in.readInt() != 0) {
+ mSourceRectHint = Rect.CREATOR.createFromParcel(in);
+ }
+ }
+
+ private PictureInPictureArgs(Rational aspectRatio, List<RemoteAction> actions,
+ Rect sourceRectHint) {
+ mAspectRatio = aspectRatio;
+ mUserActions = actions;
+ mSourceRectHint = sourceRectHint;
+ }
+
+ /**
+ * @hide
+ */
+ @Deprecated
+ public void setAspectRatio(float aspectRatio) {
+ // Temporary workaround
+ mAspectRatio = new Rational((int) (aspectRatio * 1000000000), 1000000000);
+ }
+
+ /**
+ * @hide
+ */
+ @Deprecated
+ public void setActions(List<RemoteAction> actions) {
+ if (mUserActions != null) {
+ mUserActions = null;
+ }
+ if (actions != null) {
+ mUserActions = new ArrayList<>(actions);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Deprecated
+ public void setSourceRectHint(Rect launchBounds) {
+ if (launchBounds == null) {
+ mSourceRectHint = null;
+ } else {
+ mSourceRectHint = new Rect(launchBounds);
+ }
+ }
+
+ /**
+ * Copies the set parameters from the other picture-in-picture args.
+ * @hide
+ */
+ public void copyOnlySet(PictureInPictureArgs otherArgs) {
+ if (otherArgs.hasSetAspectRatio()) {
+ mAspectRatio = otherArgs.mAspectRatio;
+ }
+ if (otherArgs.hasSetActions()) {
+ mUserActions = otherArgs.mUserActions;
+ }
+ if (otherArgs.hasSourceBoundsHint()) {
+ mSourceRectHint = new Rect(otherArgs.getSourceRectHint());
+ }
+ }
+
+ /**
+ * @return the aspect ratio. If none is set, return 0.
+ * @hide
+ */
+ public float getAspectRatio() {
+ if (mAspectRatio != null) {
+ return mAspectRatio.floatValue();
+ }
+ return 0f;
+ }
+
+ /** {@hide} */
+ public Rational getAspectRatioRational() {
+ return mAspectRatio;
+ }
+
+ /**
+ * @return whether the aspect ratio is set.
+ * @hide
+ */
+ public boolean hasSetAspectRatio() {
+ return mAspectRatio != null;
+ }
+
+ /**
+ * @return the set of user actions.
+ * @hide
+ */
+ public List<RemoteAction> getActions() {
+ return mUserActions;
+ }
+
+ /**
+ * @return whether the user actions are set.
+ * @hide
+ */
+ public boolean hasSetActions() {
+ return mUserActions != null;
+ }
+
+ /**
+ * Truncates the set of actions to the given {@param size}.
+ * @hide
+ */
+ public void truncateActions(int size) {
+ if (hasSetActions()) {
+ mUserActions = mUserActions.subList(0, Math.min(mUserActions.size(), size));
+ }
+ }
+
+ /**
+ * Sets the insets to be used with the source rect hint bounds.
+ * @hide
+ */
+ @Deprecated
+ public void setSourceRectHintInsets(Rect insets) {
+ if (insets == null) {
+ mSourceRectHintInsets = null;
+ } else {
+ mSourceRectHintInsets = new Rect(insets);
+ }
+ }
+
+ /**
+ * @return the source rect hint
+ * @hide
+ */
+ public Rect getSourceRectHint() {
+ return mSourceRectHint;
+ }
+
+ /**
+ * @return the source rect hint insets.
+ * @hide
+ */
+ public Rect getSourceRectHintInsets() {
+ return mSourceRectHintInsets;
+ }
+
+ /**
+ * @return whether there are launch bounds set
+ * @hide
+ */
+ public boolean hasSourceBoundsHint() {
+ return mSourceRectHint != null && !mSourceRectHint.isEmpty();
+ }
+
+ /**
+ * @return whether there are source rect hint insets set
+ * @hide
+ */
+ public boolean hasSourceBoundsHintInsets() {
+ return mSourceRectHintInsets != null;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (mAspectRatio != null) {
+ out.writeInt(1);
+ out.writeInt(mAspectRatio.getNumerator());
+ out.writeInt(mAspectRatio.getDenominator());
+ } else {
+ out.writeInt(0);
+ }
+ if (mUserActions != null) {
+ out.writeInt(1);
+ out.writeParcelableList(mUserActions, 0);
+ } else {
+ out.writeInt(0);
+ }
+ if (mSourceRectHint != null) {
+ out.writeInt(1);
+ mSourceRectHint.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Creator<PictureInPictureArgs> CREATOR =
+ new Creator<PictureInPictureArgs>() {
+ public PictureInPictureArgs createFromParcel(Parcel in) {
+ return new PictureInPictureArgs(in);
+ }
+ public PictureInPictureArgs[] newArray(int size) {
+ return new PictureInPictureArgs[size];
+ }
+ };
+
+ public static PictureInPictureArgs convert(PictureInPictureParams params) {
+ return new PictureInPictureArgs(params.getAspectRatioRational(), params.getActions(),
+ params.getSourceRectHint());
+ }
+
+ public static PictureInPictureParams convert(PictureInPictureArgs args) {
+ return new PictureInPictureParams(args.getAspectRatioRational(), args.getActions(),
+ args.getSourceRectHint());
+ }
+}
diff --git a/android/app/PictureInPictureParams.java b/android/app/PictureInPictureParams.java
new file mode 100644
index 00000000..7313b0d9
--- /dev/null
+++ b/android/app/PictureInPictureParams.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Rational;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a set of parameters used to initialize and update an Activity in picture-in-picture
+ * mode.
+ */
+public final class PictureInPictureParams implements Parcelable {
+
+ /**
+ * Builder class for {@link PictureInPictureParams} objects.
+ */
+ public static class Builder {
+
+ @Nullable
+ private Rational mAspectRatio;
+
+ @Nullable
+ private List<RemoteAction> mUserActions;
+
+ @Nullable
+ private Rect mSourceRectHint;
+
+ /**
+ * Sets the aspect ratio. This aspect ratio is defined as the desired width / height, and
+ * does not change upon device rotation.
+ *
+ * @param aspectRatio the new aspect ratio for the activity in picture-in-picture, must be
+ * between 2.39:1 and 1:2.39 (inclusive).
+ *
+ * @return this builder instance.
+ */
+ public Builder setAspectRatio(Rational aspectRatio) {
+ mAspectRatio = aspectRatio;
+ return this;
+ }
+
+ /**
+ * Sets the user actions. If there are more than
+ * {@link Activity#getMaxNumPictureInPictureActions()} actions, then the input list
+ * will be truncated to that number.
+ *
+ * @param actions the new actions to show in the picture-in-picture menu.
+ *
+ * @return this builder instance.
+ *
+ * @see RemoteAction
+ */
+ public Builder setActions(List<RemoteAction> actions) {
+ if (mUserActions != null) {
+ mUserActions = null;
+ }
+ if (actions != null) {
+ mUserActions = new ArrayList<>(actions);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the source bounds hint. These bounds are only used when an activity first enters
+ * picture-in-picture, and describe the bounds in window coordinates of activity entering
+ * picture-in-picture that will be visible following the transition. For the best effect,
+ * these bounds should also match the aspect ratio in the arguments.
+ *
+ * @param launchBounds window-coordinate bounds indicating the area of the activity that
+ * will still be visible following the transition into picture-in-picture (eg. the video
+ * view bounds in a video player)
+ *
+ * @return this builder instance.
+ */
+ public Builder setSourceRectHint(Rect launchBounds) {
+ if (launchBounds == null) {
+ mSourceRectHint = null;
+ } else {
+ mSourceRectHint = new Rect(launchBounds);
+ }
+ return this;
+ }
+
+ /**
+ * @return an immutable {@link PictureInPictureParams} to be used when entering or updating
+ * the activity in picture-in-picture.
+ *
+ * @see Activity#enterPictureInPictureMode(PictureInPictureParams)
+ * @see Activity#setPictureInPictureParams(PictureInPictureParams)
+ */
+ public PictureInPictureParams build() {
+ PictureInPictureParams params = new PictureInPictureParams(mAspectRatio, mUserActions,
+ mSourceRectHint);
+ return params;
+ }
+ }
+
+ /**
+ * The expected aspect ratio of the picture-in-picture.
+ */
+ @Nullable
+ private Rational mAspectRatio;
+
+ /**
+ * The set of actions that are associated with this activity when in picture-in-picture.
+ */
+ @Nullable
+ private List<RemoteAction> mUserActions;
+
+ /**
+ * The source bounds hint used when entering picture-in-picture, relative to the window bounds.
+ * We can use this internally for the transition into picture-in-picture to ensure that a
+ * particular source rect is visible throughout the whole transition.
+ */
+ @Nullable
+ private Rect mSourceRectHint;
+
+ /** {@hide} */
+ PictureInPictureParams() {
+ }
+
+ /** {@hide} */
+ PictureInPictureParams(Parcel in) {
+ if (in.readInt() != 0) {
+ mAspectRatio = new Rational(in.readInt(), in.readInt());
+ }
+ if (in.readInt() != 0) {
+ mUserActions = new ArrayList<>();
+ in.readParcelableList(mUserActions, RemoteAction.class.getClassLoader());
+ }
+ if (in.readInt() != 0) {
+ mSourceRectHint = Rect.CREATOR.createFromParcel(in);
+ }
+ }
+
+ /** {@hide} */
+ PictureInPictureParams(Rational aspectRatio, List<RemoteAction> actions,
+ Rect sourceRectHint) {
+ mAspectRatio = aspectRatio;
+ mUserActions = actions;
+ mSourceRectHint = sourceRectHint;
+ }
+
+ /**
+ * Copies the set parameters from the other picture-in-picture args.
+ * @hide
+ */
+ public void copyOnlySet(PictureInPictureParams otherArgs) {
+ if (otherArgs.hasSetAspectRatio()) {
+ mAspectRatio = otherArgs.mAspectRatio;
+ }
+ if (otherArgs.hasSetActions()) {
+ mUserActions = otherArgs.mUserActions;
+ }
+ if (otherArgs.hasSourceBoundsHint()) {
+ mSourceRectHint = new Rect(otherArgs.getSourceRectHint());
+ }
+ }
+
+ /**
+ * @return the aspect ratio. If none is set, return 0.
+ * @hide
+ */
+ public float getAspectRatio() {
+ if (mAspectRatio != null) {
+ return mAspectRatio.floatValue();
+ }
+ return 0f;
+ }
+
+ /** @hide */
+ public Rational getAspectRatioRational() {
+ return mAspectRatio;
+ }
+
+ /**
+ * @return whether the aspect ratio is set.
+ * @hide
+ */
+ public boolean hasSetAspectRatio() {
+ return mAspectRatio != null;
+ }
+
+ /**
+ * @return the set of user actions.
+ * @hide
+ */
+ public List<RemoteAction> getActions() {
+ return mUserActions;
+ }
+
+ /**
+ * @return whether the user actions are set.
+ * @hide
+ */
+ public boolean hasSetActions() {
+ return mUserActions != null;
+ }
+
+ /**
+ * Truncates the set of actions to the given {@param size}.
+ * @hide
+ */
+ public void truncateActions(int size) {
+ if (hasSetActions()) {
+ mUserActions = mUserActions.subList(0, Math.min(mUserActions.size(), size));
+ }
+ }
+
+ /**
+ * @return the source rect hint
+ * @hide
+ */
+ public Rect getSourceRectHint() {
+ return mSourceRectHint;
+ }
+
+ /**
+ * @return whether there are launch bounds set
+ * @hide
+ */
+ public boolean hasSourceBoundsHint() {
+ return mSourceRectHint != null && !mSourceRectHint.isEmpty();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (mAspectRatio != null) {
+ out.writeInt(1);
+ out.writeInt(mAspectRatio.getNumerator());
+ out.writeInt(mAspectRatio.getDenominator());
+ } else {
+ out.writeInt(0);
+ }
+ if (mUserActions != null) {
+ out.writeInt(1);
+ out.writeParcelableList(mUserActions, 0);
+ } else {
+ out.writeInt(0);
+ }
+ if (mSourceRectHint != null) {
+ out.writeInt(1);
+ mSourceRectHint.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Creator<PictureInPictureParams> CREATOR =
+ new Creator<PictureInPictureParams>() {
+ public PictureInPictureParams createFromParcel(Parcel in) {
+ return new PictureInPictureParams(in);
+ }
+ public PictureInPictureParams[] newArray(int size) {
+ return new PictureInPictureParams[size];
+ }
+ };
+}
diff --git a/android/app/Presentation.java b/android/app/Presentation.java
new file mode 100644
index 00000000..af55788e
--- /dev/null
+++ b/android/app/Presentation.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.content.Context.DISPLAY_SERVICE;
+import static android.content.Context.WINDOW_SERVICE;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.os.Handler;
+import android.os.Message;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+
+/**
+ * Base class for presentations.
+ * <p>
+ * A presentation is a special kind of dialog whose purpose is to present
+ * content on a secondary display. A {@link Presentation} is associated with
+ * the target {@link Display} at creation time and configures its context and
+ * resource configuration according to the display's metrics.
+ * </p><p>
+ * Notably, the {@link Context} of a presentation is different from the context
+ * of its containing {@link Activity}. It is important to inflate the layout
+ * of a presentation and load other resources using the presentation's own context
+ * to ensure that assets of the correct size and density for the target display
+ * are loaded.
+ * </p><p>
+ * A presentation is automatically canceled (see {@link Dialog#cancel()}) when
+ * the display to which it is attached is removed. An activity should take
+ * care of pausing and resuming whatever content is playing within the presentation
+ * whenever the activity itself is paused or resumed.
+ * </p>
+ *
+ * <h3>Choosing a presentation display</h3>
+ * <p>
+ * Before showing a {@link Presentation} it's important to choose the {@link Display}
+ * on which it will appear. Choosing a presentation display is sometimes difficult
+ * because there may be multiple displays attached. Rather than trying to guess
+ * which display is best, an application should let the system choose a suitable
+ * presentation display.
+ * </p><p>
+ * There are two main ways to choose a {@link Display}.
+ * </p>
+ *
+ * <h4>Using the media router to choose a presentation display</h4>
+ * <p>
+ * The easiest way to choose a presentation display is to use the
+ * {@link android.media.MediaRouter MediaRouter} API. The media router service keeps
+ * track of which audio and video routes are available on the system.
+ * The media router sends notifications whenever routes are selected or unselected
+ * or when the preferred presentation display of a route changes.
+ * So an application can simply watch for these notifications and show or dismiss
+ * a presentation on the preferred presentation display automatically.
+ * </p><p>
+ * The preferred presentation display is the display that the media router recommends
+ * that the application should use if it wants to show content on the secondary display.
+ * Sometimes there may not be a preferred presentation display in which
+ * case the application should show its content locally without using a presentation.
+ * </p><p>
+ * Here's how to use the media router to create and show a presentation on the preferred
+ * presentation display using {@link android.media.MediaRouter.RouteInfo#getPresentationDisplay()}.
+ * </p>
+ * <pre>
+ * MediaRouter mediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
+ * if (route != null) {
+ * Display presentationDisplay = route.getPresentationDisplay();
+ * if (presentationDisplay != null) {
+ * Presentation presentation = new MyPresentation(context, presentationDisplay);
+ * presentation.show();
+ * }
+ * }</pre>
+ * <p>
+ * The following sample code from <code>ApiDemos</code> demonstrates how to use the media
+ * router to automatically switch between showing content in the main activity and showing
+ * the content in a presentation when a presentation display is available.
+ * </p>
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/PresentationWithMediaRouterActivity.java
+ * activity}
+ *
+ * <h4>Using the display manager to choose a presentation display</h4>
+ * <p>
+ * Another way to choose a presentation display is to use the {@link DisplayManager} API
+ * directly. The display manager service provides functions to enumerate and describe all
+ * displays that are attached to the system including displays that may be used
+ * for presentations.
+ * </p><p>
+ * The display manager keeps track of all displays in the system. However, not all
+ * displays are appropriate for showing presentations. For example, if an activity
+ * attempted to show a presentation on the main display it might obscure its own content
+ * (it's like opening a dialog on top of your activity).
+ * </p><p>
+ * Here's how to identify suitable displays for showing presentations using
+ * {@link DisplayManager#getDisplays(String)} and the
+ * {@link DisplayManager#DISPLAY_CATEGORY_PRESENTATION} category.
+ * </p>
+ * <pre>
+ * DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ * Display[] presentationDisplays = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
+ * if (presentationDisplays.length > 0) {
+ * // If there is more than one suitable presentation display, then we could consider
+ * // giving the user a choice. For this example, we simply choose the first display
+ * // which is the one the system recommends as the preferred presentation display.
+ * Display display = presentationDisplays[0];
+ * Presentation presentation = new MyPresentation(context, presentationDisplay);
+ * presentation.show();
+ * }</pre>
+ * <p>
+ * The following sample code from <code>ApiDemos</code> demonstrates how to use the display
+ * manager to enumerate displays and show content on multiple presentation displays
+ * simultaneously.
+ * </p>
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/PresentationActivity.java
+ * activity}
+ *
+ * @see android.media.MediaRouter#ROUTE_TYPE_LIVE_VIDEO for information on about live
+ * video routes and how to obtain the preferred presentation display for the
+ * current media route.
+ * @see DisplayManager for information on how to enumerate displays and receive
+ * notifications when displays are added or removed.
+ */
+public class Presentation extends Dialog {
+ private static final String TAG = "Presentation";
+
+ private static final int MSG_CANCEL = 1;
+
+ private final Display mDisplay;
+ private final DisplayManager mDisplayManager;
+ private final IBinder mToken = new Binder();
+
+ /**
+ * Creates a new presentation that is attached to the specified display
+ * using the default theme.
+ *
+ * @param outerContext The context of the application that is showing the presentation.
+ * The presentation will create its own context (see {@link #getContext()}) based
+ * on this context and information about the associated display.
+ * @param display The display to which the presentation should be attached.
+ */
+ public Presentation(Context outerContext, Display display) {
+ this(outerContext, display, 0);
+ }
+
+ /**
+ * Creates a new presentation that is attached to the specified display
+ * using the optionally specified theme.
+ *
+ * @param outerContext The context of the application that is showing the presentation.
+ * The presentation will create its own context (see {@link #getContext()}) based
+ * on this context and information about the associated display.
+ * @param display The display to which the presentation should be attached.
+ * @param theme A style resource describing the theme to use for the window.
+ * See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
+ * Style and Theme Resources</a> for more information about defining and using
+ * styles. This theme is applied on top of the current theme in
+ * <var>outerContext</var>. If 0, the default presentation theme will be used.
+ */
+ public Presentation(Context outerContext, Display display, int theme) {
+ super(createPresentationContext(outerContext, display, theme), theme, false);
+
+ mDisplay = display;
+ mDisplayManager = (DisplayManager)getContext().getSystemService(DISPLAY_SERVICE);
+
+ final Window w = getWindow();
+ final WindowManager.LayoutParams attr = w.getAttributes();
+ attr.token = mToken;
+ w.setAttributes(attr);
+ w.setGravity(Gravity.FILL);
+ w.setType(TYPE_PRESENTATION);
+ setCanceledOnTouchOutside(false);
+ }
+
+ /**
+ * Gets the {@link Display} that this presentation appears on.
+ *
+ * @return The display.
+ */
+ public Display getDisplay() {
+ return mDisplay;
+ }
+
+ /**
+ * Gets the {@link Resources} that should be used to inflate the layout of this presentation.
+ * This resources object has been configured according to the metrics of the
+ * display that the presentation appears on.
+ *
+ * @return The presentation resources object.
+ */
+ public Resources getResources() {
+ return getContext().getResources();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+
+ // Since we were not watching for display changes until just now, there is a
+ // chance that the display metrics have changed. If so, we will need to
+ // dismiss the presentation immediately. This case is expected
+ // to be rare but surprising, so we'll write a log message about it.
+ if (!isConfigurationStillValid()) {
+ Log.i(TAG, "Presentation is being dismissed because the "
+ + "display metrics have changed since it was created.");
+ mHandler.sendEmptyMessage(MSG_CANCEL);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ mDisplayManager.unregisterDisplayListener(mDisplayListener);
+ super.onStop();
+ }
+
+ /**
+ * Inherited from {@link Dialog#show}. Will throw
+ * {@link android.view.WindowManager.InvalidDisplayException} if the specified secondary
+ * {@link Display} can't be found.
+ */
+ @Override
+ public void show() {
+ super.show();
+ }
+
+ /**
+ * Called by the system when the {@link Display} to which the presentation
+ * is attached has been removed.
+ *
+ * The system automatically calls {@link #cancel} to dismiss the presentation
+ * after sending this event.
+ *
+ * @see #getDisplay
+ */
+ public void onDisplayRemoved() {
+ }
+
+ /**
+ * Called by the system when the properties of the {@link Display} to which
+ * the presentation is attached have changed.
+ *
+ * If the display metrics have changed (for example, if the display has been
+ * resized or rotated), then the system automatically calls
+ * {@link #cancel} to dismiss the presentation.
+ *
+ * @see #getDisplay
+ */
+ public void onDisplayChanged() {
+ }
+
+ private void handleDisplayRemoved() {
+ onDisplayRemoved();
+ cancel();
+ }
+
+ private void handleDisplayChanged() {
+ onDisplayChanged();
+
+ // We currently do not support configuration changes for presentations
+ // (although we could add that feature with a bit more work).
+ // If the display metrics have changed in any way then the current configuration
+ // is invalid and the application must recreate the presentation to get
+ // a new context.
+ if (!isConfigurationStillValid()) {
+ Log.i(TAG, "Presentation is being dismissed because the "
+ + "display metrics have changed since it was created.");
+ cancel();
+ }
+ }
+
+ private boolean isConfigurationStillValid() {
+ DisplayMetrics dm = new DisplayMetrics();
+ mDisplay.getMetrics(dm);
+ return dm.equalsPhysical(getResources().getDisplayMetrics());
+ }
+
+ private static Context createPresentationContext(
+ Context outerContext, Display display, int theme) {
+ if (outerContext == null) {
+ throw new IllegalArgumentException("outerContext must not be null");
+ }
+ if (display == null) {
+ throw new IllegalArgumentException("display must not be null");
+ }
+
+ Context displayContext = outerContext.createDisplayContext(display);
+ if (theme == 0) {
+ TypedValue outValue = new TypedValue();
+ displayContext.getTheme().resolveAttribute(
+ com.android.internal.R.attr.presentationTheme, outValue, true);
+ theme = outValue.resourceId;
+ }
+
+ // Derive the display's window manager from the outer window manager.
+ // We do this because the outer window manager have some extra information
+ // such as the parent window, which is important if the presentation uses
+ // an application window type.
+ final WindowManagerImpl outerWindowManager =
+ (WindowManagerImpl)outerContext.getSystemService(WINDOW_SERVICE);
+ final WindowManagerImpl displayWindowManager =
+ outerWindowManager.createPresentationWindowManager(displayContext);
+ return new ContextThemeWrapper(displayContext, theme) {
+ @Override
+ public Object getSystemService(String name) {
+ if (WINDOW_SERVICE.equals(name)) {
+ return displayWindowManager;
+ }
+ return super.getSystemService(name);
+ }
+ };
+ }
+
+ private final DisplayListener mDisplayListener = new DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ if (displayId == mDisplay.getDisplayId()) {
+ handleDisplayRemoved();
+ }
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == mDisplay.getDisplayId()) {
+ handleDisplayChanged();
+ }
+ }
+ };
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CANCEL:
+ cancel();
+ break;
+ }
+ }
+ };
+}
diff --git a/android/app/ProfilerInfo.java b/android/app/ProfilerInfo.java
new file mode 100644
index 00000000..fad4798e
--- /dev/null
+++ b/android/app/ProfilerInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.util.Slog;
+
+import java.io.IOException;
+
+/**
+ * System private API for passing profiler settings.
+ *
+ * {@hide}
+ */
+public class ProfilerInfo implements Parcelable {
+
+ private static final String TAG = "ProfilerInfo";
+
+ /* Name of profile output file. */
+ public final String profileFile;
+
+ /* File descriptor for profile output file, can be null. */
+ public ParcelFileDescriptor profileFd;
+
+ /* Indicates sample profiling when nonzero, interval in microseconds. */
+ public final int samplingInterval;
+
+ /* Automatically stop the profiler when the app goes idle. */
+ public final boolean autoStopProfiler;
+
+ /*
+ * Indicates whether to stream the profiling info to the out file continuously.
+ */
+ public final boolean streamingOutput;
+
+ /**
+ * Denotes an agent (and its parameters) to attach for profiling.
+ */
+ public final String agent;
+
+ public ProfilerInfo(String filename, ParcelFileDescriptor fd, int interval, boolean autoStop,
+ boolean streaming, String agent) {
+ profileFile = filename;
+ profileFd = fd;
+ samplingInterval = interval;
+ autoStopProfiler = autoStop;
+ streamingOutput = streaming;
+ this.agent = agent;
+ }
+
+ public ProfilerInfo(ProfilerInfo in) {
+ profileFile = in.profileFile;
+ profileFd = in.profileFd;
+ samplingInterval = in.samplingInterval;
+ autoStopProfiler = in.autoStopProfiler;
+ streamingOutput = in.streamingOutput;
+ agent = in.agent;
+ }
+
+ /**
+ * Close profileFd, if it is open. The field will be null after a call to this function.
+ */
+ public void closeFd() {
+ if (profileFd != null) {
+ try {
+ profileFd.close();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failure closing profile fd", e);
+ }
+ profileFd = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ if (profileFd != null) {
+ return profileFd.describeContents();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(profileFile);
+ if (profileFd != null) {
+ out.writeInt(1);
+ profileFd.writeToParcel(out, flags);
+ } else {
+ out.writeInt(0);
+ }
+ out.writeInt(samplingInterval);
+ out.writeInt(autoStopProfiler ? 1 : 0);
+ out.writeInt(streamingOutput ? 1 : 0);
+ out.writeString(agent);
+ }
+
+ public static final Parcelable.Creator<ProfilerInfo> CREATOR =
+ new Parcelable.Creator<ProfilerInfo>() {
+ @Override
+ public ProfilerInfo createFromParcel(Parcel in) {
+ return new ProfilerInfo(in);
+ }
+
+ @Override
+ public ProfilerInfo[] newArray(int size) {
+ return new ProfilerInfo[size];
+ }
+ };
+
+ private ProfilerInfo(Parcel in) {
+ profileFile = in.readString();
+ profileFd = in.readInt() != 0 ? ParcelFileDescriptor.CREATOR.createFromParcel(in) : null;
+ samplingInterval = in.readInt();
+ autoStopProfiler = in.readInt() != 0;
+ streamingOutput = in.readInt() != 0;
+ agent = in.readString();
+ }
+}
diff --git a/android/app/ProgressDialog.java b/android/app/ProgressDialog.java
new file mode 100644
index 00000000..8a083ebc
--- /dev/null
+++ b/android/app/ProgressDialog.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.text.NumberFormat;
+
+/**
+ * A dialog showing a progress indicator and an optional text message or view.
+ * Only a text message or a view can be used at the same time.
+ *
+ * <p>The dialog can be made cancelable on back key press.</p>
+ *
+ * <p>The progress range is 0 to {@link #getMax() max}.</p>
+ *
+ * @deprecated <code>ProgressDialog</code> is a modal dialog, which prevents the
+ * user from interacting with the app. Instead of using this class, you should
+ * use a progress indicator like {@link android.widget.ProgressBar}, which can
+ * be embedded in your app's UI. Alternatively, you can use a
+ * <a href="/guide/topics/ui/notifiers/notifications.html">notification</a>
+ * to inform the user of the task's progress.
+ */
+@Deprecated
+public class ProgressDialog extends AlertDialog {
+
+ /**
+ * Creates a ProgressDialog with a circular, spinning progress
+ * bar. This is the default.
+ */
+ public static final int STYLE_SPINNER = 0;
+
+ /**
+ * Creates a ProgressDialog with a horizontal progress bar.
+ */
+ public static final int STYLE_HORIZONTAL = 1;
+
+ private ProgressBar mProgress;
+ private TextView mMessageView;
+
+ private int mProgressStyle = STYLE_SPINNER;
+ private TextView mProgressNumber;
+ private String mProgressNumberFormat;
+ private TextView mProgressPercent;
+ private NumberFormat mProgressPercentFormat;
+
+ private int mMax;
+ private int mProgressVal;
+ private int mSecondaryProgressVal;
+ private int mIncrementBy;
+ private int mIncrementSecondaryBy;
+ private Drawable mProgressDrawable;
+ private Drawable mIndeterminateDrawable;
+ private CharSequence mMessage;
+ private boolean mIndeterminate;
+
+ private boolean mHasStarted;
+ private Handler mViewUpdateHandler;
+
+ /**
+ * Creates a Progress dialog.
+ *
+ * @param context the parent context
+ */
+ public ProgressDialog(Context context) {
+ super(context);
+ initFormats();
+ }
+
+ /**
+ * Creates a Progress dialog.
+ *
+ * @param context the parent context
+ * @param theme the resource ID of the theme against which to inflate
+ * this dialog, or {@code 0} to use the parent
+ * {@code context}'s default alert dialog theme
+ */
+ public ProgressDialog(Context context, int theme) {
+ super(context, theme);
+ initFormats();
+ }
+
+ private void initFormats() {
+ mProgressNumberFormat = "%1d/%2d";
+ mProgressPercentFormat = NumberFormat.getPercentInstance();
+ mProgressPercentFormat.setMaximumFractionDigits(0);
+ }
+
+ /**
+ * Creates and shows a ProgressDialog.
+ *
+ * @param context the parent context
+ * @param title the title text for the dialog's window
+ * @param message the text to be displayed in the dialog
+ * @return the ProgressDialog
+ */
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message) {
+ return show(context, title, message, false);
+ }
+
+ /**
+ * Creates and shows a ProgressDialog.
+ *
+ * @param context the parent context
+ * @param title the title text for the dialog's window
+ * @param message the text to be displayed in the dialog
+ * @param indeterminate true if the dialog should be {@link #setIndeterminate(boolean)
+ * indeterminate}, false otherwise
+ * @return the ProgressDialog
+ */
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate) {
+ return show(context, title, message, indeterminate, false, null);
+ }
+
+ /**
+ * Creates and shows a ProgressDialog.
+ *
+ * @param context the parent context
+ * @param title the title text for the dialog's window
+ * @param message the text to be displayed in the dialog
+ * @param indeterminate true if the dialog should be {@link #setIndeterminate(boolean)
+ * indeterminate}, false otherwise
+ * @param cancelable true if the dialog is {@link #setCancelable(boolean) cancelable},
+ * false otherwise
+ * @return the ProgressDialog
+ */
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate, boolean cancelable) {
+ return show(context, title, message, indeterminate, cancelable, null);
+ }
+
+ /**
+ * Creates and shows a ProgressDialog.
+ *
+ * @param context the parent context
+ * @param title the title text for the dialog's window
+ * @param message the text to be displayed in the dialog
+ * @param indeterminate true if the dialog should be {@link #setIndeterminate(boolean)
+ * indeterminate}, false otherwise
+ * @param cancelable true if the dialog is {@link #setCancelable(boolean) cancelable},
+ * false otherwise
+ * @param cancelListener the {@link #setOnCancelListener(OnCancelListener) listener}
+ * to be invoked when the dialog is canceled
+ * @return the ProgressDialog
+ */
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate,
+ boolean cancelable, OnCancelListener cancelListener) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(title);
+ dialog.setMessage(message);
+ dialog.setIndeterminate(indeterminate);
+ dialog.setCancelable(cancelable);
+ dialog.setOnCancelListener(cancelListener);
+ dialog.show();
+ return dialog;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ TypedArray a = mContext.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.AlertDialog,
+ com.android.internal.R.attr.alertDialogStyle, 0);
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+
+ /* Use a separate handler to update the text views as they
+ * must be updated on the same thread that created them.
+ */
+ mViewUpdateHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+
+ /* Update the number and percent */
+ int progress = mProgress.getProgress();
+ int max = mProgress.getMax();
+ if (mProgressNumberFormat != null) {
+ String format = mProgressNumberFormat;
+ mProgressNumber.setText(String.format(format, progress, max));
+ } else {
+ mProgressNumber.setText("");
+ }
+ if (mProgressPercentFormat != null) {
+ double percent = (double) progress / (double) max;
+ SpannableString tmp = new SpannableString(mProgressPercentFormat.format(percent));
+ tmp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
+ 0, tmp.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mProgressPercent.setText(tmp);
+ } else {
+ mProgressPercent.setText("");
+ }
+ }
+ };
+ View view = inflater.inflate(a.getResourceId(
+ com.android.internal.R.styleable.AlertDialog_horizontalProgressLayout,
+ R.layout.alert_dialog_progress), null);
+ mProgress = (ProgressBar) view.findViewById(R.id.progress);
+ mProgressNumber = (TextView) view.findViewById(R.id.progress_number);
+ mProgressPercent = (TextView) view.findViewById(R.id.progress_percent);
+ setView(view);
+ } else {
+ View view = inflater.inflate(a.getResourceId(
+ com.android.internal.R.styleable.AlertDialog_progressLayout,
+ R.layout.progress_dialog), null);
+ mProgress = (ProgressBar) view.findViewById(R.id.progress);
+ mMessageView = (TextView) view.findViewById(R.id.message);
+ setView(view);
+ }
+ a.recycle();
+ if (mMax > 0) {
+ setMax(mMax);
+ }
+ if (mProgressVal > 0) {
+ setProgress(mProgressVal);
+ }
+ if (mSecondaryProgressVal > 0) {
+ setSecondaryProgress(mSecondaryProgressVal);
+ }
+ if (mIncrementBy > 0) {
+ incrementProgressBy(mIncrementBy);
+ }
+ if (mIncrementSecondaryBy > 0) {
+ incrementSecondaryProgressBy(mIncrementSecondaryBy);
+ }
+ if (mProgressDrawable != null) {
+ setProgressDrawable(mProgressDrawable);
+ }
+ if (mIndeterminateDrawable != null) {
+ setIndeterminateDrawable(mIndeterminateDrawable);
+ }
+ if (mMessage != null) {
+ setMessage(mMessage);
+ }
+ setIndeterminate(mIndeterminate);
+ onProgressChanged();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHasStarted = true;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mHasStarted = false;
+ }
+
+ /**
+ * Sets the current progress.
+ *
+ * @param value the current progress, a value between 0 and {@link #getMax()}
+ *
+ * @see ProgressBar#setProgress(int)
+ */
+ public void setProgress(int value) {
+ if (mHasStarted) {
+ mProgress.setProgress(value);
+ onProgressChanged();
+ } else {
+ mProgressVal = value;
+ }
+ }
+
+ /**
+ * Sets the secondary progress.
+ *
+ * @param secondaryProgress the current secondary progress, a value between 0 and
+ * {@link #getMax()}
+ *
+ * @see ProgressBar#setSecondaryProgress(int)
+ */
+ public void setSecondaryProgress(int secondaryProgress) {
+ if (mProgress != null) {
+ mProgress.setSecondaryProgress(secondaryProgress);
+ onProgressChanged();
+ } else {
+ mSecondaryProgressVal = secondaryProgress;
+ }
+ }
+
+ /**
+ * Gets the current progress.
+ *
+ * @return the current progress, a value between 0 and {@link #getMax()}
+ */
+ public int getProgress() {
+ if (mProgress != null) {
+ return mProgress.getProgress();
+ }
+ return mProgressVal;
+ }
+
+ /**
+ * Gets the current secondary progress.
+ *
+ * @return the current secondary progress, a value between 0 and {@link #getMax()}
+ */
+ public int getSecondaryProgress() {
+ if (mProgress != null) {
+ return mProgress.getSecondaryProgress();
+ }
+ return mSecondaryProgressVal;
+ }
+
+ /**
+ * Gets the maximum allowed progress value. The default value is 100.
+ *
+ * @return the maximum value
+ */
+ public int getMax() {
+ if (mProgress != null) {
+ return mProgress.getMax();
+ }
+ return mMax;
+ }
+
+ /**
+ * Sets the maximum allowed progress value.
+ */
+ public void setMax(int max) {
+ if (mProgress != null) {
+ mProgress.setMax(max);
+ onProgressChanged();
+ } else {
+ mMax = max;
+ }
+ }
+
+ /**
+ * Increments the current progress value.
+ *
+ * @param diff the amount by which the current progress will be incremented,
+ * up to {@link #getMax()}
+ */
+ public void incrementProgressBy(int diff) {
+ if (mProgress != null) {
+ mProgress.incrementProgressBy(diff);
+ onProgressChanged();
+ } else {
+ mIncrementBy += diff;
+ }
+ }
+
+ /**
+ * Increments the current secondary progress value.
+ *
+ * @param diff the amount by which the current secondary progress will be incremented,
+ * up to {@link #getMax()}
+ */
+ public void incrementSecondaryProgressBy(int diff) {
+ if (mProgress != null) {
+ mProgress.incrementSecondaryProgressBy(diff);
+ onProgressChanged();
+ } else {
+ mIncrementSecondaryBy += diff;
+ }
+ }
+
+ /**
+ * Sets the drawable to be used to display the progress value.
+ *
+ * @param d the drawable to be used
+ *
+ * @see ProgressBar#setProgressDrawable(Drawable)
+ */
+ public void setProgressDrawable(Drawable d) {
+ if (mProgress != null) {
+ mProgress.setProgressDrawable(d);
+ } else {
+ mProgressDrawable = d;
+ }
+ }
+
+ /**
+ * Sets the drawable to be used to display the indeterminate progress value.
+ *
+ * @param d the drawable to be used
+ *
+ * @see ProgressBar#setProgressDrawable(Drawable)
+ * @see #setIndeterminate(boolean)
+ */
+ public void setIndeterminateDrawable(Drawable d) {
+ if (mProgress != null) {
+ mProgress.setIndeterminateDrawable(d);
+ } else {
+ mIndeterminateDrawable = d;
+ }
+ }
+
+ /**
+ * Change the indeterminate mode for this ProgressDialog. In indeterminate
+ * mode, the progress is ignored and the dialog shows an infinite
+ * animation instead.
+ *
+ * <p><strong>Note:</strong> A ProgressDialog with style {@link #STYLE_SPINNER}
+ * is always indeterminate and will ignore this setting.</p>
+ *
+ * @param indeterminate true to enable indeterminate mode, false otherwise
+ *
+ * @see #setProgressStyle(int)
+ */
+ public void setIndeterminate(boolean indeterminate) {
+ if (mProgress != null) {
+ mProgress.setIndeterminate(indeterminate);
+ } else {
+ mIndeterminate = indeterminate;
+ }
+ }
+
+ /**
+ * Whether this ProgressDialog is in indeterminate mode.
+ *
+ * @return true if the dialog is in indeterminate mode, false otherwise
+ */
+ public boolean isIndeterminate() {
+ if (mProgress != null) {
+ return mProgress.isIndeterminate();
+ }
+ return mIndeterminate;
+ }
+
+ @Override
+ public void setMessage(CharSequence message) {
+ if (mProgress != null) {
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+ super.setMessage(message);
+ } else {
+ mMessageView.setText(message);
+ }
+ } else {
+ mMessage = message;
+ }
+ }
+
+ /**
+ * Sets the style of this ProgressDialog, either {@link #STYLE_SPINNER} or
+ * {@link #STYLE_HORIZONTAL}. The default is {@link #STYLE_SPINNER}.
+ *
+ * <p><strong>Note:</strong> A ProgressDialog with style {@link #STYLE_SPINNER}
+ * is always indeterminate and will ignore the {@link #setIndeterminate(boolean)
+ * indeterminate} setting.</p>
+ *
+ * @param style the style of this ProgressDialog, either {@link #STYLE_SPINNER} or
+ * {@link #STYLE_HORIZONTAL}
+ */
+ public void setProgressStyle(int style) {
+ mProgressStyle = style;
+ }
+
+ /**
+ * Change the format of the small text showing current and maximum units
+ * of progress. The default is "%1d/%2d".
+ * Should not be called during the number is progressing.
+ * @param format A string passed to {@link String#format String.format()};
+ * use "%1d" for the current number and "%2d" for the maximum. If null,
+ * nothing will be shown.
+ */
+ public void setProgressNumberFormat(String format) {
+ mProgressNumberFormat = format;
+ onProgressChanged();
+ }
+
+ /**
+ * Change the format of the small text showing the percentage of progress.
+ * The default is
+ * {@link NumberFormat#getPercentInstance() NumberFormat.getPercentageInstnace().}
+ * Should not be called during the number is progressing.
+ * @param format An instance of a {@link NumberFormat} to generate the
+ * percentage text. If null, nothing will be shown.
+ */
+ public void setProgressPercentFormat(NumberFormat format) {
+ mProgressPercentFormat = format;
+ onProgressChanged();
+ }
+
+ private void onProgressChanged() {
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+ if (mViewUpdateHandler != null && !mViewUpdateHandler.hasMessages(0)) {
+ mViewUpdateHandler.sendEmptyMessage(0);
+ }
+ }
+ }
+}
diff --git a/android/app/QueuedWork.java b/android/app/QueuedWork.java
new file mode 100644
index 00000000..56338f58
--- /dev/null
+++ b/android/app/QueuedWork.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.StrictMode;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ExponentiallyBucketedHistogram;
+
+import java.util.LinkedList;
+
+/**
+ * Internal utility class to keep track of process-global work that's outstanding and hasn't been
+ * finished yet.
+ *
+ * New work will be {@link #queue queued}.
+ *
+ * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
+ * This is used to make sure the work has been finished.
+ *
+ * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
+ * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
+ * other things in the future.
+ *
+ * The queued asynchronous work is performed on a separate, dedicated thread.
+ *
+ * @hide
+ */
+public class QueuedWork {
+ private static final String LOG_TAG = QueuedWork.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ /** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
+ private static final long DELAY = 100;
+
+ /** If a {@link #waitToFinish()} takes more than {@value #MAX_WAIT_TIME_MILLIS} ms, warn */
+ private static final long MAX_WAIT_TIME_MILLIS = 512;
+
+ /** Lock for this class */
+ private static final Object sLock = new Object();
+
+ /**
+ * Used to make sure that only one thread is processing work items at a time. This means that
+ * they are processed in the order added.
+ *
+ * This is separate from {@link #sLock} as this is held the whole time while work is processed
+ * and we do not want to stall the whole class.
+ */
+ private static Object sProcessingWork = new Object();
+
+ /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
+
+ /** {@link #getHandler() Lazily} created handler */
+ @GuardedBy("sLock")
+ private static Handler sHandler = null;
+
+ /** Work queued via {@link #queue} */
+ @GuardedBy("sLock")
+ private static final LinkedList<Runnable> sWork = new LinkedList<>();
+
+ /** If new work can be delayed or not */
+ @GuardedBy("sLock")
+ private static boolean sCanDelay = true;
+
+ /** Time (and number of instances) waited for work to get processed */
+ @GuardedBy("sLock")
+ private final static ExponentiallyBucketedHistogram
+ mWaitTimes = new ExponentiallyBucketedHistogram(
+ 16);
+ private static int mNumWaits = 0;
+
+ /**
+ * Lazily create a handler on a separate thread.
+ *
+ * @return the handler
+ */
+ private static Handler getHandler() {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("queued-work-looper",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ handlerThread.start();
+
+ sHandler = new QueuedWorkHandler(handlerThread.getLooper());
+ }
+ return sHandler;
+ }
+ }
+
+ /**
+ * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
+ *
+ * Used by SharedPreferences$Editor#startCommit().
+ *
+ * Note that this doesn't actually start it running. This is just a scratch set for callers
+ * doing async work to keep updated with what's in-flight. In the common case, caller code
+ * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
+ * these Runnables are run is from {@link #waitToFinish}.
+ *
+ * @param finisher The runnable to add as finisher
+ */
+ public static void addFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.add(finisher);
+ }
+ }
+
+ /**
+ * Remove a previously {@link #addFinisher added} finisher-runnable.
+ *
+ * @param finisher The runnable to remove.
+ */
+ public static void removeFinisher(Runnable finisher) {
+ synchronized (sLock) {
+ sFinishers.remove(finisher);
+ }
+ }
+
+ /**
+ * Trigger queued work to be processed immediately. The queued work is processed on a separate
+ * thread asynchronous. While doing that run and process all finishers on this thread. The
+ * finishers can be implemented in a way to check weather the queued work is finished.
+ *
+ * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
+ * after Service command handling, etc. (so async work is never lost)
+ */
+ public static void waitToFinish() {
+ long startTime = System.currentTimeMillis();
+ boolean hadMessages = false;
+
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
+ // Delayed work will be processed at processPendingWork() below
+ handler.removeMessages(QueuedWorkHandler.MSG_RUN);
+
+ if (DEBUG) {
+ hadMessages = true;
+ Log.d(LOG_TAG, "waiting");
+ }
+ }
+
+ // We should not delay any work as this might delay the finishers
+ sCanDelay = false;
+ }
+
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ processPendingWork();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+
+ try {
+ while (true) {
+ Runnable finisher;
+
+ synchronized (sLock) {
+ finisher = sFinishers.poll();
+ }
+
+ if (finisher == null) {
+ break;
+ }
+
+ finisher.run();
+ }
+ } finally {
+ sCanDelay = true;
+ }
+
+ synchronized (sLock) {
+ long waitTime = System.currentTimeMillis() - startTime;
+
+ if (waitTime > 0 || hadMessages) {
+ mWaitTimes.add(Long.valueOf(waitTime).intValue());
+ mNumWaits++;
+
+ if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
+ mWaitTimes.log(LOG_TAG, "waited: ");
+ }
+ }
+ }
+ }
+
+ /**
+ * Queue a work-runnable for processing asynchronously.
+ *
+ * @param work The new runnable to process
+ * @param shouldDelay If the message should be delayed
+ */
+ public static void queue(Runnable work, boolean shouldDelay) {
+ Handler handler = getHandler();
+
+ synchronized (sLock) {
+ sWork.add(work);
+
+ if (shouldDelay && sCanDelay) {
+ handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
+ } else {
+ handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
+ }
+ }
+ }
+
+ /**
+ * @return True iff there is any {@link #queue async work queued}.
+ */
+ public static boolean hasPendingWork() {
+ synchronized (sLock) {
+ return !sWork.isEmpty();
+ }
+ }
+
+ private static void processPendingWork() {
+ long startTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ synchronized (sProcessingWork) {
+ LinkedList<Runnable> work;
+
+ synchronized (sLock) {
+ work = (LinkedList<Runnable>) sWork.clone();
+ sWork.clear();
+
+ // Remove all msg-s as all work will be processed now
+ getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
+ }
+
+ if (work.size() > 0) {
+ for (Runnable w : work) {
+ w.run();
+ }
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+ +(System.currentTimeMillis() - startTime) + " ms");
+ }
+ }
+ }
+ }
+
+ private static class QueuedWorkHandler extends Handler {
+ static final int MSG_RUN = 1;
+
+ QueuedWorkHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_RUN) {
+ processPendingWork();
+ }
+ }
+ }
+}
diff --git a/android/app/RecoverableSecurityException.java b/android/app/RecoverableSecurityException.java
new file mode 100644
index 00000000..6747004e
--- /dev/null
+++ b/android/app/RecoverableSecurityException.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Specialization of {@link SecurityException} that contains additional
+ * information about how to involve the end user to recover from the exception.
+ * <p>
+ * This exception is only appropriate where there is a concrete action the user
+ * can take to recover and make forward progress, such as confirming or entering
+ * authentication credentials, or granting access.
+ * <p>
+ * If the receiving app is actively involved with the user, it should present
+ * the contained recovery details to help the user make forward progress. The
+ * {@link #showAsDialog(Activity)} and
+ * {@link #showAsNotification(Context, String)} methods are provided as a
+ * convenience, but receiving apps are encouraged to use
+ * {@link #getUserMessage()} and {@link #getUserAction()} to integrate in a more
+ * natural way if relevant.
+ * <p class="note">
+ * Note: legacy code that receives this exception may treat it as a general
+ * {@link SecurityException}, and thus there is no guarantee that the messages
+ * contained will be shown to the end user.
+ *
+ * @hide
+ */
+public final class RecoverableSecurityException extends SecurityException implements Parcelable {
+ private static final String TAG = "RecoverableSecurityException";
+
+ private final CharSequence mUserMessage;
+ private final RemoteAction mUserAction;
+
+ /** {@hide} */
+ public RecoverableSecurityException(Parcel in) {
+ this(new SecurityException(in.readString()), in.readCharSequence(),
+ RemoteAction.CREATOR.createFromParcel(in));
+ }
+
+ /**
+ * Create an instance ready to be thrown.
+ *
+ * @param cause original cause with details designed for engineering
+ * audiences.
+ * @param userMessage short message describing the issue for end user
+ * audiences, which may be shown in a notification or dialog.
+ * This should be localized and less than 64 characters. For
+ * example: <em>PIN required to access Document.pdf</em>
+ * @param userAction primary action that will initiate the recovery. The
+ * title should be localized and less than 24 characters. For
+ * example: <em>Enter PIN</em>. This action must launch an
+ * activity that is expected to set
+ * {@link Activity#setResult(int)} before finishing to
+ * communicate the final status of the recovery. For example,
+ * apps that observe {@link Activity#RESULT_OK} may choose to
+ * immediately retry their operation.
+ */
+ public RecoverableSecurityException(Throwable cause, CharSequence userMessage,
+ RemoteAction userAction) {
+ super(cause.getMessage());
+ mUserMessage = Preconditions.checkNotNull(userMessage);
+ mUserAction = Preconditions.checkNotNull(userAction);
+ }
+
+ /** {@hide} */
+ @Deprecated
+ public RecoverableSecurityException(Throwable cause, CharSequence userMessage,
+ CharSequence userActionTitle, PendingIntent userAction) {
+ this(cause, userMessage,
+ new RemoteAction(
+ Icon.createWithResource("android",
+ com.android.internal.R.drawable.ic_restart),
+ userActionTitle, userActionTitle, userAction));
+ }
+
+ /**
+ * Return short message describing the issue for end user audiences, which
+ * may be shown in a notification or dialog.
+ */
+ public CharSequence getUserMessage() {
+ return mUserMessage;
+ }
+
+ /**
+ * Return primary action that will initiate the recovery.
+ */
+ public RemoteAction getUserAction() {
+ return mUserAction;
+ }
+
+ /** @removed */
+ @Deprecated
+ public void showAsNotification(Context context) {
+ final NotificationManager nm = context.getSystemService(NotificationManager.class);
+
+ // Create a channel per-sender, since we don't want one poorly behaved
+ // remote app to cause all of our notifications to be blocked
+ final String channelId = TAG + "_" + mUserAction.getActionIntent().getCreatorUid();
+ nm.createNotificationChannel(new NotificationChannel(channelId, TAG,
+ NotificationManager.IMPORTANCE_DEFAULT));
+
+ showAsNotification(context, channelId);
+ }
+
+ /**
+ * Convenience method that will show a very simple notification populated
+ * with the details from this exception.
+ * <p>
+ * If you want more flexibility over retrying your original operation once
+ * the user action has finished, consider presenting your own UI that uses
+ * {@link Activity#startIntentSenderForResult} to launch the
+ * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()}
+ * when requested. If the result of that activity is
+ * {@link Activity#RESULT_OK}, you should consider retrying.
+ * <p>
+ * This method will only display the most recent exception from any single
+ * remote UID; notifications from older exceptions will always be replaced.
+ *
+ * @param channelId the {@link NotificationChannel} to use, which must have
+ * been already created using
+ * {@link NotificationManager#createNotificationChannel}.
+ */
+ public void showAsNotification(Context context, String channelId) {
+ final NotificationManager nm = context.getSystemService(NotificationManager.class);
+ final Notification.Builder builder = new Notification.Builder(context, channelId)
+ .setSmallIcon(com.android.internal.R.drawable.ic_print_error)
+ .setContentTitle(mUserAction.getTitle())
+ .setContentText(mUserMessage)
+ .setContentIntent(mUserAction.getActionIntent())
+ .setCategory(Notification.CATEGORY_ERROR);
+ nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build());
+ }
+
+ /**
+ * Convenience method that will show a very simple dialog populated with the
+ * details from this exception.
+ * <p>
+ * If you want more flexibility over retrying your original operation once
+ * the user action has finished, consider presenting your own UI that uses
+ * {@link Activity#startIntentSenderForResult} to launch the
+ * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()}
+ * when requested. If the result of that activity is
+ * {@link Activity#RESULT_OK}, you should consider retrying.
+ * <p>
+ * This method will only display the most recent exception from any single
+ * remote UID; dialogs from older exceptions will always be replaced.
+ */
+ public void showAsDialog(Activity activity) {
+ final LocalDialog dialog = new LocalDialog();
+ final Bundle args = new Bundle();
+ args.putParcelable(TAG, this);
+ dialog.setArguments(args);
+
+ final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid();
+ final FragmentManager fm = activity.getFragmentManager();
+ final FragmentTransaction ft = fm.beginTransaction();
+ final Fragment old = fm.findFragmentByTag(tag);
+ if (old != null) {
+ ft.remove(old);
+ }
+ ft.add(dialog, tag);
+ ft.commitAllowingStateLoss();
+ }
+
+ /**
+ * Implementation detail for
+ * {@link RecoverableSecurityException#showAsDialog(Activity)}; needs to
+ * remain static to be recreated across orientation changes.
+ *
+ * @hide
+ */
+ public static class LocalDialog extends DialogFragment {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final RecoverableSecurityException e = getArguments().getParcelable(TAG);
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(e.mUserMessage)
+ .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> {
+ try {
+ e.mUserAction.getActionIntent().send();
+ } catch (PendingIntent.CanceledException ignored) {
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(getMessage());
+ dest.writeCharSequence(mUserMessage);
+ mUserAction.writeToParcel(dest, flags);
+ }
+
+ public static final Creator<RecoverableSecurityException> CREATOR =
+ new Creator<RecoverableSecurityException>() {
+ @Override
+ public RecoverableSecurityException createFromParcel(Parcel source) {
+ return new RecoverableSecurityException(source);
+ }
+
+ @Override
+ public RecoverableSecurityException[] newArray(int size) {
+ return new RecoverableSecurityException[size];
+ }
+ };
+}
diff --git a/android/app/RemoteAction.java b/android/app/RemoteAction.java
new file mode 100644
index 00000000..e7fe407b
--- /dev/null
+++ b/android/app/RemoteAction.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.PrintWriter;
+
+/**
+ * Represents a remote action that can be called from another process. The action can have an
+ * associated visualization including metadata like an icon or title.
+ */
+public final class RemoteAction implements Parcelable {
+
+ private static final String TAG = "RemoteAction";
+
+ private final Icon mIcon;
+ private final CharSequence mTitle;
+ private final CharSequence mContentDescription;
+ private final PendingIntent mActionIntent;
+ private boolean mEnabled;
+
+ RemoteAction(Parcel in) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mActionIntent = PendingIntent.CREATOR.createFromParcel(in);
+ mEnabled = in.readBoolean();
+ }
+
+ public RemoteAction(@NonNull Icon icon, @NonNull CharSequence title,
+ @NonNull CharSequence contentDescription, @NonNull PendingIntent intent) {
+ if (icon == null || title == null || contentDescription == null || intent == null) {
+ throw new IllegalArgumentException("Expected icon, title, content description and " +
+ "action callback");
+ }
+ mIcon = icon;
+ mTitle = title;
+ mContentDescription = contentDescription;
+ mActionIntent = intent;
+ mEnabled = true;
+ }
+
+ /**
+ * Sets whether this action is enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Return whether this action is enabled.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Return an icon representing the action.
+ */
+ public @NonNull Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Return an title representing the action.
+ */
+ public @NonNull CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Return a content description representing the action.
+ */
+ public @NonNull CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Return the action intent.
+ */
+ public @NonNull PendingIntent getActionIntent() {
+ return mActionIntent;
+ }
+
+ @Override
+ public RemoteAction clone() {
+ RemoteAction action = new RemoteAction(mIcon, mTitle, mContentDescription, mActionIntent);
+ action.setEnabled(mEnabled);
+ return action;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mIcon.writeToParcel(out, 0);
+ TextUtils.writeToParcel(mTitle, out, flags);
+ TextUtils.writeToParcel(mContentDescription, out, flags);
+ mActionIntent.writeToParcel(out, flags);
+ out.writeBoolean(mEnabled);
+ }
+
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix);
+ pw.print("title=" + mTitle);
+ pw.print(" enabled=" + mEnabled);
+ pw.print(" contentDescription=" + mContentDescription);
+ pw.print(" icon=" + mIcon);
+ pw.print(" action=" + mActionIntent.getIntent());
+ pw.println();
+ }
+
+ public static final Parcelable.Creator<RemoteAction> CREATOR =
+ new Parcelable.Creator<RemoteAction>() {
+ public RemoteAction createFromParcel(Parcel in) {
+ return new RemoteAction(in);
+ }
+ public RemoteAction[] newArray(int size) {
+ return new RemoteAction[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/android/app/RemoteInput.java b/android/app/RemoteInput.java
new file mode 100644
index 00000000..8ab19c06
--- /dev/null
+++ b/android/app/RemoteInput.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@code RemoteInput} object specifies input to be collected from a user to be passed along with
+ * an intent inside a {@link android.app.PendingIntent} that is sent.
+ * Always use {@link RemoteInput.Builder} to create instances of this class.
+ * <p class="note"> See
+ * <a href="{@docRoot}wear/notifications/remote-input.html">Receiving Voice Input from
+ * a Notification</a> for more information on how to use this class.
+ *
+ * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action},
+ * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}.
+ * Users are prompted to input a response when they trigger the action. The results are sent along
+ * with the intent and can be retrieved with the result key (provided to the {@link Builder}
+ * constructor) from the Bundle returned by {@link #getResultsFromIntent}.
+ *
+ * <pre class="prettyprint">
+ * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
+ * Notification.Action action = new Notification.Action.Builder(
+ * R.drawable.reply, &quot;Reply&quot;, actionIntent)
+ * <b>.addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
+ * .setLabel("Quick reply").build()</b>)
+ * .build();</pre>
+ *
+ * <p>When the {@link android.app.PendingIntent} is fired, the intent inside will contain the
+ * input results if collected. To access these results, use the {@link #getResultsFromIntent}
+ * function. The result values will present under the result key passed to the {@link Builder}
+ * constructor.
+ *
+ * <pre class="prettyprint">
+ * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
+ * Bundle results = RemoteInput.getResultsFromIntent(intent);
+ * if (results != null) {
+ * CharSequence quickReplyResult = results.getCharSequence(KEY_QUICK_REPLY_TEXT);
+ * }</pre>
+ */
+public final class RemoteInput implements Parcelable {
+ /** Label used to denote the clip data type used for remote input transport */
+ public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
+
+ /** Extra added to a clip data intent object to hold the text results bundle. */
+ public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
+
+ /** Extra added to a clip data intent object to hold the data results bundle. */
+ private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
+ "android.remoteinput.dataTypeResultsData";
+
+ // Flags bitwise-ored to mFlags
+ private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
+
+ // Default value for flags integer
+ private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT;
+
+ private final String mResultKey;
+ private final CharSequence mLabel;
+ private final CharSequence[] mChoices;
+ private final int mFlags;
+ private final Bundle mExtras;
+ private final ArraySet<String> mAllowedDataTypes;
+
+ private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
+ int flags, Bundle extras, ArraySet<String> allowedDataTypes) {
+ this.mResultKey = resultKey;
+ this.mLabel = label;
+ this.mChoices = choices;
+ this.mFlags = flags;
+ this.mExtras = extras;
+ this.mAllowedDataTypes = allowedDataTypes;
+ }
+
+ /**
+ * Get the key that the result of this input will be set in from the Bundle returned by
+ * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
+ */
+ public String getResultKey() {
+ return mResultKey;
+ }
+
+ /**
+ * Get the label to display to users when collecting this input.
+ */
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Get possible input choices. This can be {@code null} if there are no choices to present.
+ */
+ public CharSequence[] getChoices() {
+ return mChoices;
+ }
+
+ /**
+ * Get possible non-textual inputs that are accepted.
+ * This can be {@code null} if the input does not accept non-textual values.
+ * See {@link Builder#setAllowDataType}.
+ */
+ public Set<String> getAllowedDataTypes() {
+ return mAllowedDataTypes;
+ }
+
+ /**
+ * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput}
+ * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes is
+ * non-null and not empty.
+ */
+ public boolean isDataOnly() {
+ return !getAllowFreeFormInput()
+ && (getChoices() == null || getChoices().length == 0)
+ && !getAllowedDataTypes().isEmpty();
+ }
+
+ /**
+ * Get whether or not users can provide an arbitrary value for
+ * input. If you set this to {@code false}, users must select one of the
+ * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
+ * if you set this to false and {@link #getChoices} returns {@code null} or empty.
+ */
+ public boolean getAllowFreeFormInput() {
+ return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0;
+ }
+
+ /**
+ * Get additional metadata carried around with this remote input.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Builder class for {@link RemoteInput} objects.
+ */
+ public static final class Builder {
+ private final String mResultKey;
+ private CharSequence mLabel;
+ private CharSequence[] mChoices;
+ private int mFlags = DEFAULT_FLAGS;
+ private Bundle mExtras = new Bundle();
+ private final ArraySet<String> mAllowedDataTypes = new ArraySet<>();
+
+ /**
+ * Create a builder object for {@link RemoteInput} objects.
+ * @param resultKey the Bundle key that refers to this input when collected from the user
+ */
+ public Builder(String resultKey) {
+ if (resultKey == null) {
+ throw new IllegalArgumentException("Result key can't be null");
+ }
+ mResultKey = resultKey;
+ }
+
+ /**
+ * Set a label to be displayed to the user when collecting this input.
+ * @param label The label to show to users when they input a response.
+ * @return this object for method chaining
+ */
+ public Builder setLabel(CharSequence label) {
+ mLabel = Notification.safeCharSequence(label);
+ return this;
+ }
+
+ /**
+ * Specifies choices available to the user to satisfy this input.
+ * @param choices an array of pre-defined choices for users input.
+ * You must provide a non-null and non-empty array if
+ * you disabled free form input using {@link #setAllowFreeFormInput}.
+ * @return this object for method chaining
+ */
+ public Builder setChoices(CharSequence[] choices) {
+ if (choices == null) {
+ mChoices = null;
+ } else {
+ mChoices = new CharSequence[choices.length];
+ for (int i = 0; i < choices.length; i++) {
+ mChoices[i] = Notification.safeCharSequence(choices[i]);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Specifies whether the user can provide arbitrary values. This allows an input
+ * to accept non-textual values. Examples of usage are an input that wants audio
+ * or an image.
+ *
+ * @param mimeType A mime type that results are allowed to come in.
+ * Be aware that text results (see {@link #setAllowFreeFormInput}
+ * are allowed by default. If you do not want text results you will have to
+ * pass false to {@code setAllowFreeFormInput}.
+ * @param doAllow Whether the mime type should be allowed or not.
+ * @return this object for method chaining
+ */
+ public Builder setAllowDataType(String mimeType, boolean doAllow) {
+ if (doAllow) {
+ mAllowedDataTypes.add(mimeType);
+ } else {
+ mAllowedDataTypes.remove(mimeType);
+ }
+ return this;
+ }
+
+ /**
+ * Specifies whether the user can provide arbitrary text values.
+ *
+ * @param allowFreeFormTextInput The default is {@code true}.
+ * If you specify {@code false}, you must either provide a non-null
+ * and non-empty array to {@link #setChoices}, or enable a data result
+ * in {@code setAllowDataType}. Otherwise an
+ * {@link IllegalArgumentException} is thrown.
+ * @return this object for method chaining
+ */
+ public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
+ setFlag(mFlags, allowFreeFormTextInput);
+ return this;
+ }
+
+ /**
+ * Merge additional metadata into this builder.
+ *
+ * <p>Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see RemoteInput#getExtras
+ */
+ public Builder addExtras(Bundle extras) {
+ if (extras != null) {
+ mExtras.putAll(extras);
+ }
+ return this;
+ }
+
+ /**
+ * Get the metadata Bundle used by this Builder.
+ *
+ * <p>The returned Bundle is shared with this Builder.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ /**
+ * Combine all of the options that have been set and return a new {@link RemoteInput}
+ * object.
+ */
+ public RemoteInput build() {
+ return new RemoteInput(
+ mResultKey, mLabel, mChoices, mFlags, mExtras, mAllowedDataTypes);
+ }
+ }
+
+ private RemoteInput(Parcel in) {
+ mResultKey = in.readString();
+ mLabel = in.readCharSequence();
+ mChoices = in.readCharSequenceArray();
+ mFlags = in.readInt();
+ mExtras = in.readBundle();
+ mAllowedDataTypes = (ArraySet<String>) in.readArraySet(null);
+ }
+
+ /**
+ * Similar as {@link #getResultsFromIntent} but retrieves data results for a
+ * specific RemoteInput result. To retrieve a value use:
+ * <pre>
+ * {@code
+ * Map<String, Uri> results =
+ * RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
+ * if (results != null) {
+ * Uri data = results.get(MIME_TYPE_OF_INTEREST);
+ * }
+ * }
+ * </pre>
+ * @param intent The intent object that fired in response to an action or content intent
+ * which also had one or more remote input requested.
+ * @param remoteInputResultKey The result key for the RemoteInput you want results for.
+ */
+ public static Map<String, Uri> getDataResultsFromIntent(
+ Intent intent, String remoteInputResultKey) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ return null;
+ }
+ Map<String, Uri> results = new HashMap<>();
+ Bundle extras = clipDataIntent.getExtras();
+ for (String key : extras.keySet()) {
+ if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
+ String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
+ if (mimeType == null || mimeType.isEmpty()) {
+ continue;
+ }
+ Bundle bundle = clipDataIntent.getBundleExtra(key);
+ String uriStr = bundle.getString(remoteInputResultKey);
+ if (uriStr == null || uriStr.isEmpty()) {
+ continue;
+ }
+ results.put(mimeType, Uri.parse(uriStr));
+ }
+ }
+ return results.isEmpty() ? null : results;
+ }
+
+ /**
+ * Get the remote input text results bundle from an intent. The returned Bundle will
+ * contain a key/value for every result key populated with text by remote input collector.
+ * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For non-text
+ * results use {@link #getDataResultsFromIntent}.
+ * @param intent The intent object that fired in response to an action or content intent
+ * which also had one or more remote input requested.
+ */
+ public static Bundle getResultsFromIntent(Intent intent) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ return null;
+ }
+ return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
+ }
+
+ /**
+ * Populate an intent object with the text results gathered from remote input. This method
+ * should only be called by remote input collection services when sending results to a
+ * pending intent.
+ * @param remoteInputs The remote inputs for which results are being provided
+ * @param intent The intent to add remote inputs to. The {@link ClipData}
+ * field of the intent will be modified to contain the results.
+ * @param results A bundle holding the remote input results. This bundle should
+ * be populated with keys matching the result keys specified in
+ * {@code remoteInputs} with values being the CharSequence results per key.
+ */
+ public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
+ Bundle results) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ clipDataIntent = new Intent(); // First time we've added a result.
+ }
+ Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA);
+ if (resultsBundle == null) {
+ resultsBundle = new Bundle();
+ }
+ for (RemoteInput remoteInput : remoteInputs) {
+ Object result = results.get(remoteInput.getResultKey());
+ if (result instanceof CharSequence) {
+ resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
+ }
+ }
+ clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
+ intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
+ }
+
+ /**
+ * Same as {@link #addResultsToIntent} but for setting data results. This is used
+ * for inputs that accept non-textual results (see {@link Builder#setAllowDataType}).
+ * Only one result can be provided for every mime type accepted by the RemoteInput.
+ * If multiple inputs of the same mime type are expected then multiple RemoteInputs
+ * should be used.
+ *
+ * @param remoteInput The remote input for which results are being provided
+ * @param intent The intent to add remote input results to. The {@link ClipData}
+ * field of the intent will be modified to contain the results.
+ * @param results A map of mime type to the Uri result for that mime type.
+ */
+ public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
+ Map<String, Uri> results) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ clipDataIntent = new Intent(); // First time we've added a result.
+ }
+ for (Map.Entry<String, Uri> entry : results.entrySet()) {
+ String mimeType = entry.getKey();
+ Uri uri = entry.getValue();
+ if (mimeType == null) {
+ continue;
+ }
+ Bundle resultsBundle =
+ clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
+ if (resultsBundle == null) {
+ resultsBundle = new Bundle();
+ }
+ resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
+
+ clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
+ }
+ intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
+ }
+
+ private static String getExtraResultsKeyForData(String mimeType) {
+ return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mResultKey);
+ out.writeCharSequence(mLabel);
+ out.writeCharSequenceArray(mChoices);
+ out.writeInt(mFlags);
+ out.writeBundle(mExtras);
+ out.writeArraySet(mAllowedDataTypes);
+ }
+
+ public static final Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() {
+ @Override
+ public RemoteInput createFromParcel(Parcel in) {
+ return new RemoteInput(in);
+ }
+
+ @Override
+ public RemoteInput[] newArray(int size) {
+ return new RemoteInput[size];
+ }
+ };
+
+ private static Intent getClipDataIntentFromIntent(Intent intent) {
+ ClipData clipData = intent.getClipData();
+ if (clipData == null) {
+ return null;
+ }
+ ClipDescription clipDescription = clipData.getDescription();
+ if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
+ return null;
+ }
+ if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
+ return null;
+ }
+ return clipData.getItemAt(0).getIntent();
+ }
+}
diff --git a/android/app/ResourcesManager.java b/android/app/ResourcesManager.java
new file mode 100644
index 00000000..fb11272d
--- /dev/null
+++ b/android/app/ResourcesManager.java
@@ -0,0 +1,1040 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.ActivityThread.DEBUG_CONFIGURATION;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo;
+import android.content.res.AssetManager;
+import android.content.res.CompatResources;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.ResourcesImpl;
+import android.content.res.ResourcesKey;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.IBinder;
+import android.os.Trace;
+import android.util.ArrayMap;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.function.Predicate;
+
+/** @hide */
+public class ResourcesManager {
+ static final String TAG = "ResourcesManager";
+ private static final boolean DEBUG = false;
+
+ private static ResourcesManager sResourcesManager;
+
+ /**
+ * Predicate that returns true if a WeakReference is gc'ed.
+ */
+ private static final Predicate<WeakReference<Resources>> sEmptyReferencePredicate =
+ new Predicate<WeakReference<Resources>>() {
+ @Override
+ public boolean test(WeakReference<Resources> weakRef) {
+ return weakRef == null || weakRef.get() == null;
+ }
+ };
+
+ /**
+ * The global compatibility settings.
+ */
+ private CompatibilityInfo mResCompatibilityInfo;
+
+ /**
+ * The global configuration upon which all Resources are based. Multi-window Resources
+ * apply their overrides to this configuration.
+ */
+ private final Configuration mResConfiguration = new Configuration();
+
+ /**
+ * A mapping of ResourceImpls and their configurations. These are heavy weight objects
+ * which should be reused as much as possible.
+ */
+ private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
+ new ArrayMap<>();
+
+ /**
+ * A list of Resource references that can be reused.
+ */
+ private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
+
+ /**
+ * Resources and base configuration override associated with an Activity.
+ */
+ private static class ActivityResources {
+ public final Configuration overrideConfig = new Configuration();
+ public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
+ }
+
+ /**
+ * Each Activity may has a base override configuration that is applied to each Resources object,
+ * which in turn may have their own override configuration specified.
+ */
+ private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences =
+ new WeakHashMap<>();
+
+ /**
+ * A cache of DisplayId, DisplayAdjustments to Display.
+ */
+ private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>>
+ mAdjustedDisplays = new ArrayMap<>();
+
+ public static ResourcesManager getInstance() {
+ synchronized (ResourcesManager.class) {
+ if (sResourcesManager == null) {
+ sResourcesManager = new ResourcesManager();
+ }
+ return sResourcesManager;
+ }
+ }
+
+ /**
+ * Invalidate and destroy any resources that reference content under the
+ * given filesystem path. Typically used when unmounting a storage device to
+ * try as hard as possible to release any open FDs.
+ */
+ public void invalidatePath(String path) {
+ synchronized (this) {
+ int count = 0;
+ for (int i = 0; i < mResourceImpls.size();) {
+ final ResourcesKey key = mResourceImpls.keyAt(i);
+ if (key.isPathReferenced(path)) {
+ cleanupResourceImpl(key);
+ count++;
+ } else {
+ i++;
+ }
+ }
+ Log.i(TAG, "Invalidated " + count + " asset managers that referenced " + path);
+ }
+ }
+
+ public Configuration getConfiguration() {
+ synchronized (this) {
+ return mResConfiguration;
+ }
+ }
+
+ DisplayMetrics getDisplayMetrics() {
+ return getDisplayMetrics(Display.DEFAULT_DISPLAY,
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS);
+ }
+
+ /**
+ * Protected so that tests can override and returns something a fixed value.
+ */
+ @VisibleForTesting
+ protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) {
+ DisplayMetrics dm = new DisplayMetrics();
+ final Display display = getAdjustedDisplay(displayId, da);
+ if (display != null) {
+ display.getMetrics(dm);
+ } else {
+ dm.setToDefaults();
+ }
+ return dm;
+ }
+
+ private static void applyNonDefaultDisplayMetricsToConfiguration(
+ @NonNull DisplayMetrics dm, @NonNull Configuration config) {
+ config.touchscreen = Configuration.TOUCHSCREEN_NOTOUCH;
+ config.densityDpi = dm.densityDpi;
+ config.screenWidthDp = (int) (dm.widthPixels / dm.density);
+ config.screenHeightDp = (int) (dm.heightPixels / dm.density);
+ int sl = Configuration.resetScreenLayout(config.screenLayout);
+ if (dm.widthPixels > dm.heightPixels) {
+ config.orientation = Configuration.ORIENTATION_LANDSCAPE;
+ config.screenLayout = Configuration.reduceScreenLayout(sl,
+ config.screenWidthDp, config.screenHeightDp);
+ } else {
+ config.orientation = Configuration.ORIENTATION_PORTRAIT;
+ config.screenLayout = Configuration.reduceScreenLayout(sl,
+ config.screenHeightDp, config.screenWidthDp);
+ }
+ config.smallestScreenWidthDp = config.screenWidthDp; // assume screen does not rotate
+ config.compatScreenWidthDp = config.screenWidthDp;
+ config.compatScreenHeightDp = config.screenHeightDp;
+ config.compatSmallestScreenWidthDp = config.smallestScreenWidthDp;
+ }
+
+ public boolean applyCompatConfigurationLocked(int displayDensity,
+ @NonNull Configuration compatConfiguration) {
+ if (mResCompatibilityInfo != null && !mResCompatibilityInfo.supportsScreen()) {
+ mResCompatibilityInfo.applyToConfiguration(displayDensity, compatConfiguration);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns an adjusted {@link Display} object based on the inputs or null if display isn't
+ * available. This method is only used within {@link ResourcesManager} to calculate display
+ * metrics based on a set {@link DisplayAdjustments}. All other usages should instead call
+ * {@link ResourcesManager#getAdjustedDisplay(int, Resources)}.
+ *
+ * @param displayId display Id.
+ * @param displayAdjustments display adjustments.
+ */
+ private Display getAdjustedDisplay(final int displayId,
+ @Nullable DisplayAdjustments displayAdjustments) {
+ final DisplayAdjustments displayAdjustmentsCopy = (displayAdjustments != null)
+ ? new DisplayAdjustments(displayAdjustments) : new DisplayAdjustments();
+ final Pair<Integer, DisplayAdjustments> key =
+ Pair.create(displayId, displayAdjustmentsCopy);
+ synchronized (this) {
+ WeakReference<Display> wd = mAdjustedDisplays.get(key);
+ if (wd != null) {
+ final Display display = wd.get();
+ if (display != null) {
+ return display;
+ }
+ }
+ final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
+ if (dm == null) {
+ // may be null early in system startup
+ return null;
+ }
+ final Display display = dm.getCompatibleDisplay(displayId, key.second);
+ if (display != null) {
+ mAdjustedDisplays.put(key, new WeakReference<>(display));
+ }
+ return display;
+ }
+ }
+
+ /**
+ * Returns an adjusted {@link Display} object based on the inputs or null if display isn't
+ * available.
+ *
+ * @param displayId display Id.
+ * @param resources The {@link Resources} backing the display adjustments.
+ */
+ public Display getAdjustedDisplay(final int displayId, Resources resources) {
+ synchronized (this) {
+ final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
+ if (dm == null) {
+ // may be null early in system startup
+ return null;
+ }
+ return dm.getCompatibleDisplay(displayId, resources);
+ }
+ }
+
+ private void cleanupResourceImpl(ResourcesKey removedKey) {
+ // Remove resource key to resource impl mapping and flush cache
+ final ResourcesImpl res = mResourceImpls.remove(removedKey).get();
+
+ if (res != null) {
+ res.flushLayoutCache();
+ }
+ }
+
+ /**
+ * Creates an AssetManager from the paths within the ResourcesKey.
+ *
+ * This can be overridden in tests so as to avoid creating a real AssetManager with
+ * real APK paths.
+ * @param key The key containing the resource paths to add to the AssetManager.
+ * @return a new AssetManager.
+ */
+ @VisibleForTesting
+ protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
+ AssetManager assets = new AssetManager();
+
+ // resDir can be null if the 'android' package is creating a new Resources object.
+ // This is fine, since each AssetManager automatically loads the 'android' package
+ // already.
+ if (key.mResDir != null) {
+ if (assets.addAssetPath(key.mResDir) == 0) {
+ Log.e(TAG, "failed to add asset path " + key.mResDir);
+ return null;
+ }
+ }
+
+ if (key.mSplitResDirs != null) {
+ for (final String splitResDir : key.mSplitResDirs) {
+ if (assets.addAssetPath(splitResDir) == 0) {
+ Log.e(TAG, "failed to add split asset path " + splitResDir);
+ return null;
+ }
+ }
+ }
+
+ if (key.mOverlayDirs != null) {
+ for (final String idmapPath : key.mOverlayDirs) {
+ assets.addOverlayPath(idmapPath);
+ }
+ }
+
+ if (key.mLibDirs != null) {
+ for (final String libDir : key.mLibDirs) {
+ if (libDir.endsWith(".apk")) {
+ // Avoid opening files we know do not have resources,
+ // like code-only .jar files.
+ if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
+ Log.w(TAG, "Asset path '" + libDir +
+ "' does not exist or contains no resources.");
+ }
+ }
+ }
+ }
+ return assets;
+ }
+
+ private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) {
+ Configuration config;
+ final boolean isDefaultDisplay = (key.mDisplayId == Display.DEFAULT_DISPLAY);
+ final boolean hasOverrideConfig = key.hasOverrideConfiguration();
+ if (!isDefaultDisplay || hasOverrideConfig) {
+ config = new Configuration(getConfiguration());
+ if (!isDefaultDisplay) {
+ applyNonDefaultDisplayMetricsToConfiguration(dm, config);
+ }
+ if (hasOverrideConfig) {
+ config.updateFrom(key.mOverrideConfiguration);
+ if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration);
+ }
+ } else {
+ config = getConfiguration();
+ }
+ return config;
+ }
+
+ private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
+ final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
+ daj.setCompatibilityInfo(key.mCompatInfo);
+
+ final AssetManager assets = createAssetManager(key);
+ if (assets == null) {
+ return null;
+ }
+
+ final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
+ final Configuration config = generateConfig(key, dm);
+ final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
+
+ if (DEBUG) {
+ Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
+ }
+ return impl;
+ }
+
+ /**
+ * Finds a cached ResourcesImpl object that matches the given ResourcesKey.
+ *
+ * @param key The key to match.
+ * @return a ResourcesImpl if the key matches a cache entry, null otherwise.
+ */
+ private @Nullable ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
+ WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key);
+ ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
+ if (impl != null && impl.getAssets().isUpToDate()) {
+ return impl;
+ }
+ return null;
+ }
+
+ /**
+ * Finds a cached ResourcesImpl object that matches the given ResourcesKey, or
+ * creates a new one and caches it for future use.
+ * @param key The key to match.
+ * @return a ResourcesImpl object matching the key.
+ */
+ private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
+ @NonNull ResourcesKey key) {
+ ResourcesImpl impl = findResourcesImplForKeyLocked(key);
+ if (impl == null) {
+ impl = createResourcesImpl(key);
+ if (impl != null) {
+ mResourceImpls.put(key, new WeakReference<>(impl));
+ }
+ }
+ return impl;
+ }
+
+ /**
+ * Find the ResourcesKey that this ResourcesImpl object is associated with.
+ * @return the ResourcesKey or null if none was found.
+ */
+ private @Nullable ResourcesKey findKeyForResourceImplLocked(
+ @NonNull ResourcesImpl resourceImpl) {
+ final int refCount = mResourceImpls.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+ ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
+ if (impl != null && resourceImpl == impl) {
+ return mResourceImpls.keyAt(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check if activity resources have same override config as the provided on.
+ * @param activityToken The Activity that resources should be associated with.
+ * @param overrideConfig The override configuration to be checked for equality with.
+ * @return true if activity resources override config matches the provided one or they are both
+ * null, false otherwise.
+ */
+ boolean isSameResourcesOverrideConfig(@Nullable IBinder activityToken,
+ @Nullable Configuration overrideConfig) {
+ synchronized (this) {
+ final ActivityResources activityResources
+ = activityToken != null ? mActivityResourceReferences.get(activityToken) : null;
+ if (activityResources == null) {
+ return overrideConfig == null;
+ } else {
+ // The two configurations must either be equal or publicly equivalent to be
+ // considered the same.
+ return Objects.equals(activityResources.overrideConfig, overrideConfig)
+ || (overrideConfig != null && activityResources.overrideConfig != null
+ && 0 == overrideConfig.diffPublicOnly(
+ activityResources.overrideConfig));
+ }
+ }
+ }
+
+ private ActivityResources getOrCreateActivityResourcesStructLocked(
+ @NonNull IBinder activityToken) {
+ ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
+ if (activityResources == null) {
+ activityResources = new ActivityResources();
+ mActivityResourceReferences.put(activityToken, activityResources);
+ }
+ return activityResources;
+ }
+
+ /**
+ * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
+ * or the class loader is different.
+ */
+ private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
+ @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
+ @NonNull CompatibilityInfo compatInfo) {
+ final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+ activityToken);
+
+ final int refCount = activityResources.activityResources.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<Resources> weakResourceRef = activityResources.activityResources.get(i);
+ Resources resources = weakResourceRef.get();
+
+ if (resources != null
+ && Objects.equals(resources.getClassLoader(), classLoader)
+ && resources.getImpl() == impl) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing ref=" + resources);
+ }
+ return resources;
+ }
+ }
+
+ Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
+ : new Resources(classLoader);
+ resources.setImpl(impl);
+ activityResources.activityResources.add(new WeakReference<>(resources));
+ if (DEBUG) {
+ Slog.d(TAG, "- creating new ref=" + resources);
+ Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
+ }
+ return resources;
+ }
+
+ /**
+ * Gets an existing Resources object if the class loader and ResourcesImpl are the same,
+ * otherwise creates a new Resources object.
+ */
+ private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
+ @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
+ // Find an existing Resources that has this ResourcesImpl set.
+ final int refCount = mResourceReferences.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
+ Resources resources = weakResourceRef.get();
+ if (resources != null &&
+ Objects.equals(resources.getClassLoader(), classLoader) &&
+ resources.getImpl() == impl) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing ref=" + resources);
+ }
+ return resources;
+ }
+ }
+
+ // Create a new Resources reference and use the existing ResourcesImpl object.
+ Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
+ : new Resources(classLoader);
+ resources.setImpl(impl);
+ mResourceReferences.add(new WeakReference<>(resources));
+ if (DEBUG) {
+ Slog.d(TAG, "- creating new ref=" + resources);
+ Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
+ }
+ return resources;
+ }
+
+ /**
+ * Creates base resources for an Activity. Calls to
+ * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration,
+ * CompatibilityInfo, ClassLoader)} with the same activityToken will have their override
+ * configurations merged with the one specified here.
+ *
+ * @param activityToken Represents an Activity.
+ * @param resDir The base resource path. Can be null (only framework resources will be loaded).
+ * @param splitResDirs An array of split resource paths. Can be null.
+ * @param overlayDirs An array of overlay paths. Can be null.
+ * @param libDirs An array of resource library paths. Can be null.
+ * @param displayId The ID of the display for which to create the resources.
+ * @param overrideConfig The configuration to apply on top of the base configuration. Can be
+ * null. This provides the base override for this Activity.
+ * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is
+ * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}.
+ * @param classLoader The class loader to use when inflating Resources. If null, the
+ * {@link ClassLoader#getSystemClassLoader()} is used.
+ * @return a Resources object from which to access resources.
+ */
+ public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
+ @Nullable String resDir,
+ @Nullable String[] splitResDirs,
+ @Nullable String[] overlayDirs,
+ @Nullable String[] libDirs,
+ int displayId,
+ @Nullable Configuration overrideConfig,
+ @NonNull CompatibilityInfo compatInfo,
+ @Nullable ClassLoader classLoader) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
+ "ResourcesManager#createBaseActivityResources");
+ final ResourcesKey key = new ResourcesKey(
+ resDir,
+ splitResDirs,
+ overlayDirs,
+ libDirs,
+ displayId,
+ overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
+ compatInfo);
+ classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
+
+ if (DEBUG) {
+ Slog.d(TAG, "createBaseActivityResources activity=" + activityToken
+ + " with key=" + key);
+ }
+
+ synchronized (this) {
+ // Force the creation of an ActivityResourcesStruct.
+ getOrCreateActivityResourcesStructLocked(activityToken);
+ }
+
+ // Update any existing Activity Resources references.
+ updateResourcesForActivity(activityToken, overrideConfig, displayId,
+ false /* movedToDifferentDisplay */);
+
+ // Now request an actual Resources object.
+ return getOrCreateResources(activityToken, key, classLoader);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
+ * Gets an existing Resources object set with a ResourcesImpl object matching the given key,
+ * or creates one if it doesn't exist.
+ *
+ * @param activityToken The Activity this Resources object should be associated with.
+ * @param key The key describing the parameters of the ResourcesImpl object.
+ * @param classLoader The classloader to use for the Resources object.
+ * If null, {@link ClassLoader#getSystemClassLoader()} is used.
+ * @return A Resources object that gets updated when
+ * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
+ * is called.
+ */
+ private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
+ @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
+ synchronized (this) {
+ if (DEBUG) {
+ Throwable here = new Throwable();
+ here.fillInStackTrace();
+ Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
+ }
+
+ if (activityToken != null) {
+ final ActivityResources activityResources =
+ getOrCreateActivityResourcesStructLocked(activityToken);
+
+ // Clean up any dead references so they don't pile up.
+ ArrayUtils.unstableRemoveIf(activityResources.activityResources,
+ sEmptyReferencePredicate);
+
+ // Rebase the key's override config on top of the Activity's base override.
+ if (key.hasOverrideConfiguration()
+ && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
+ final Configuration temp = new Configuration(activityResources.overrideConfig);
+ temp.updateFrom(key.mOverrideConfiguration);
+ key.mOverrideConfiguration.setTo(temp);
+ }
+
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
+ if (resourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing impl=" + resourcesImpl);
+ }
+ return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
+ resourcesImpl, key.mCompatInfo);
+ }
+
+ // We will create the ResourcesImpl object outside of holding this lock.
+
+ } else {
+ // Clean up any dead references so they don't pile up.
+ ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
+
+ // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
+ if (resourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing impl=" + resourcesImpl);
+ }
+ return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
+ }
+
+ // We will create the ResourcesImpl object outside of holding this lock.
+ }
+ }
+
+ // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
+ ResourcesImpl resourcesImpl = createResourcesImpl(key);
+ if (resourcesImpl == null) {
+ return null;
+ }
+
+ synchronized (this) {
+ ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
+ if (existingResourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
+ + " new impl=" + resourcesImpl);
+ }
+ resourcesImpl.getAssets().close();
+ resourcesImpl = existingResourcesImpl;
+ } else {
+ // Add this ResourcesImpl to the cache.
+ mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
+ }
+
+ final Resources resources;
+ if (activityToken != null) {
+ resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
+ resourcesImpl, key.mCompatInfo);
+ } else {
+ resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
+ }
+ return resources;
+ }
+ }
+
+ /**
+ * Gets or creates a new Resources object associated with the IBinder token. References returned
+ * by this method live as long as the Activity, meaning they can be cached and used by the
+ * Activity even after a configuration change. If any other parameter is changed
+ * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object
+ * is updated and handed back to the caller. However, changing the class loader will result in a
+ * new Resources object.
+ * <p/>
+ * If activityToken is null, a cached Resources object will be returned if it matches the
+ * input parameters. Otherwise a new Resources object that satisfies these parameters is
+ * returned.
+ *
+ * @param activityToken Represents an Activity. If null, global resources are assumed.
+ * @param resDir The base resource path. Can be null (only framework resources will be loaded).
+ * @param splitResDirs An array of split resource paths. Can be null.
+ * @param overlayDirs An array of overlay paths. Can be null.
+ * @param libDirs An array of resource library paths. Can be null.
+ * @param displayId The ID of the display for which to create the resources.
+ * @param overrideConfig The configuration to apply on top of the base configuration. Can be
+ * null. Mostly used with Activities that are in multi-window which may override width and
+ * height properties from the base config.
+ * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is
+ * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}.
+ * @param classLoader The class loader to use when inflating Resources. If null, the
+ * {@link ClassLoader#getSystemClassLoader()} is used.
+ * @return a Resources object from which to access resources.
+ */
+ public @Nullable Resources getResources(@Nullable IBinder activityToken,
+ @Nullable String resDir,
+ @Nullable String[] splitResDirs,
+ @Nullable String[] overlayDirs,
+ @Nullable String[] libDirs,
+ int displayId,
+ @Nullable Configuration overrideConfig,
+ @NonNull CompatibilityInfo compatInfo,
+ @Nullable ClassLoader classLoader) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
+ final ResourcesKey key = new ResourcesKey(
+ resDir,
+ splitResDirs,
+ overlayDirs,
+ libDirs,
+ displayId,
+ overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
+ compatInfo);
+ classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
+ return getOrCreateResources(activityToken, key, classLoader);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
+ * Updates an Activity's Resources object with overrideConfig. The Resources object
+ * that was previously returned by
+ * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration,
+ * CompatibilityInfo, ClassLoader)} is
+ * still valid and will have the updated configuration.
+ * @param activityToken The Activity token.
+ * @param overrideConfig The configuration override to update.
+ * @param displayId Id of the display where activity currently resides.
+ * @param movedToDifferentDisplay Indicates if the activity was moved to different display.
+ */
+ public void updateResourcesForActivity(@NonNull IBinder activityToken,
+ @Nullable Configuration overrideConfig, int displayId,
+ boolean movedToDifferentDisplay) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
+ "ResourcesManager#updateResourcesForActivity");
+ synchronized (this) {
+ final ActivityResources activityResources =
+ getOrCreateActivityResourcesStructLocked(activityToken);
+
+ if (Objects.equals(activityResources.overrideConfig, overrideConfig)
+ && !movedToDifferentDisplay) {
+ // They are the same and no change of display id, no work to do.
+ return;
+ }
+
+ // Grab a copy of the old configuration so we can create the delta's of each
+ // Resources object associated with this Activity.
+ final Configuration oldConfig = new Configuration(activityResources.overrideConfig);
+
+ // Update the Activity's base override.
+ if (overrideConfig != null) {
+ activityResources.overrideConfig.setTo(overrideConfig);
+ } else {
+ activityResources.overrideConfig.unset();
+ }
+
+ if (DEBUG) {
+ Throwable here = new Throwable();
+ here.fillInStackTrace();
+ Slog.d(TAG, "updating resources override for activity=" + activityToken
+ + " from oldConfig="
+ + Configuration.resourceQualifierString(oldConfig)
+ + " to newConfig="
+ + Configuration.resourceQualifierString(
+ activityResources.overrideConfig) + " displayId=" + displayId,
+ here);
+ }
+
+ final boolean activityHasOverrideConfig =
+ !activityResources.overrideConfig.equals(Configuration.EMPTY);
+
+ // Rebase each Resources associated with this Activity.
+ final int refCount = activityResources.activityResources.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<Resources> weakResRef = activityResources.activityResources.get(
+ i);
+ Resources resources = weakResRef.get();
+ if (resources == null) {
+ continue;
+ }
+
+ // Extract the ResourcesKey that was last used to create the Resources for this
+ // activity.
+ final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
+ if (oldKey == null) {
+ Slog.e(TAG, "can't find ResourcesKey for resources impl="
+ + resources.getImpl());
+ continue;
+ }
+
+ // Build the new override configuration for this ResourcesKey.
+ final Configuration rebasedOverrideConfig = new Configuration();
+ if (overrideConfig != null) {
+ rebasedOverrideConfig.setTo(overrideConfig);
+ }
+
+ if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) {
+ // Generate a delta between the old base Activity override configuration and
+ // the actual final override configuration that was used to figure out the
+ // real delta this Resources object wanted.
+ Configuration overrideOverrideConfig = Configuration.generateDelta(
+ oldConfig, oldKey.mOverrideConfiguration);
+ rebasedOverrideConfig.updateFrom(overrideOverrideConfig);
+ }
+
+ // Create the new ResourcesKey with the rebased override config.
+ final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir,
+ oldKey.mSplitResDirs,
+ oldKey.mOverlayDirs, oldKey.mLibDirs, displayId,
+ rebasedOverrideConfig, oldKey.mCompatInfo);
+
+ if (DEBUG) {
+ Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey
+ + " to newKey=" + newKey + ", displayId=" + displayId);
+ }
+
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey);
+ if (resourcesImpl == null) {
+ resourcesImpl = createResourcesImpl(newKey);
+ if (resourcesImpl != null) {
+ mResourceImpls.put(newKey, new WeakReference<>(resourcesImpl));
+ }
+ }
+
+ if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
+ // Set the ResourcesImpl, updating it for all users of this Resources
+ // object.
+ resources.setImpl(resourcesImpl);
+ }
+ }
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ public final boolean applyConfigurationToResourcesLocked(@NonNull Configuration config,
+ @Nullable CompatibilityInfo compat) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
+ "ResourcesManager#applyConfigurationToResourcesLocked");
+
+ if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) {
+ if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Skipping new config: curSeq="
+ + mResConfiguration.seq + ", newSeq=" + config.seq);
+ return false;
+ }
+ int changes = mResConfiguration.updateFrom(config);
+ // Things might have changed in display manager, so clear the cached displays.
+ mAdjustedDisplays.clear();
+
+ DisplayMetrics defaultDisplayMetrics = getDisplayMetrics();
+
+ if (compat != null && (mResCompatibilityInfo == null ||
+ !mResCompatibilityInfo.equals(compat))) {
+ mResCompatibilityInfo = compat;
+ changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT
+ | ActivityInfo.CONFIG_SCREEN_SIZE
+ | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+ }
+
+ Resources.updateSystemConfiguration(config, defaultDisplayMetrics, compat);
+
+ ApplicationPackageManager.configurationChanged();
+ //Slog.i(TAG, "Configuration changed in " + currentPackageName());
+
+ Configuration tmpConfig = null;
+
+ for (int i = mResourceImpls.size() - 1; i >= 0; i--) {
+ ResourcesKey key = mResourceImpls.keyAt(i);
+ WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+ ResourcesImpl r = weakImplRef != null ? weakImplRef.get() : null;
+ if (r != null) {
+ if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "
+ + r + " config to: " + config);
+ int displayId = key.mDisplayId;
+ boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
+ DisplayMetrics dm = defaultDisplayMetrics;
+ final boolean hasOverrideConfiguration = key.hasOverrideConfiguration();
+ if (!isDefaultDisplay || hasOverrideConfiguration) {
+ if (tmpConfig == null) {
+ tmpConfig = new Configuration();
+ }
+ tmpConfig.setTo(config);
+
+ // Get new DisplayMetrics based on the DisplayAdjustments given
+ // to the ResourcesImpl. Update a copy if the CompatibilityInfo
+ // changed, because the ResourcesImpl object will handle the
+ // update internally.
+ DisplayAdjustments daj = r.getDisplayAdjustments();
+ if (compat != null) {
+ daj = new DisplayAdjustments(daj);
+ daj.setCompatibilityInfo(compat);
+ }
+ dm = getDisplayMetrics(displayId, daj);
+
+ if (!isDefaultDisplay) {
+ applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig);
+ }
+
+ if (hasOverrideConfiguration) {
+ tmpConfig.updateFrom(key.mOverrideConfiguration);
+ }
+ r.updateConfiguration(tmpConfig, dm, compat);
+ } else {
+ r.updateConfiguration(config, dm, compat);
+ }
+ //Slog.i(TAG, "Updated app resources " + v.getKey()
+ // + " " + r + ": " + r.getConfiguration());
+ } else {
+ //Slog.i(TAG, "Removing old resources " + v.getKey());
+ mResourceImpls.removeAt(i);
+ }
+ }
+
+ return changes != 0;
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
+ * Appends the library asset path to any ResourcesImpl object that contains the main
+ * assetPath.
+ * @param assetPath The main asset path for which to add the library asset path.
+ * @param libAsset The library asset path to add.
+ */
+ public void appendLibAssetForMainAssetPath(String assetPath, String libAsset) {
+ synchronized (this) {
+ // Record which ResourcesImpl need updating
+ // (and what ResourcesKey they should update to).
+ final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();
+
+ final int implCount = mResourceImpls.size();
+ for (int i = 0; i < implCount; i++) {
+ final ResourcesKey key = mResourceImpls.keyAt(i);
+ final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+ final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
+ if (impl != null && Objects.equals(key.mResDir, assetPath)) {
+ if (!ArrayUtils.contains(key.mLibDirs, libAsset)) {
+ final int newLibAssetCount = 1 +
+ (key.mLibDirs != null ? key.mLibDirs.length : 0);
+ final String[] newLibAssets = new String[newLibAssetCount];
+ if (key.mLibDirs != null) {
+ System.arraycopy(key.mLibDirs, 0, newLibAssets, 0, key.mLibDirs.length);
+ }
+ newLibAssets[newLibAssetCount - 1] = libAsset;
+
+ updatedResourceKeys.put(impl, new ResourcesKey(
+ key.mResDir,
+ key.mSplitResDirs,
+ key.mOverlayDirs,
+ newLibAssets,
+ key.mDisplayId,
+ key.mOverrideConfiguration,
+ key.mCompatInfo));
+ }
+ }
+ }
+
+ redirectResourcesToNewImplLocked(updatedResourceKeys);
+ }
+ }
+
+ // TODO(adamlesinski): Make this accept more than just overlay directories.
+ final void applyNewResourceDirsLocked(@NonNull final String baseCodePath,
+ @Nullable final String[] newResourceDirs) {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
+ "ResourcesManager#applyNewResourceDirsLocked");
+
+ final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();
+ final int implCount = mResourceImpls.size();
+ for (int i = 0; i < implCount; i++) {
+ final ResourcesKey key = mResourceImpls.keyAt(i);
+ final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+ final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
+ if (impl != null && (key.mResDir == null || key.mResDir.equals(baseCodePath))) {
+ updatedResourceKeys.put(impl, new ResourcesKey(
+ key.mResDir,
+ key.mSplitResDirs,
+ newResourceDirs,
+ key.mLibDirs,
+ key.mDisplayId,
+ key.mOverrideConfiguration,
+ key.mCompatInfo));
+ }
+ }
+
+ redirectResourcesToNewImplLocked(updatedResourceKeys);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ private void redirectResourcesToNewImplLocked(
+ @NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
+ // Bail early if there is no work to do.
+ if (updatedResourceKeys.isEmpty()) {
+ return;
+ }
+
+ // Update any references to ResourcesImpl that require reloading.
+ final int resourcesCount = mResourceReferences.size();
+ for (int i = 0; i < resourcesCount; i++) {
+ final WeakReference<Resources> ref = mResourceReferences.get(i);
+ final Resources r = ref != null ? ref.get() : null;
+ if (r != null) {
+ final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
+ if (key != null) {
+ final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
+ if (impl == null) {
+ throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
+ }
+ r.setImpl(impl);
+ }
+ }
+ }
+
+ // Update any references to ResourcesImpl that require reloading for each Activity.
+ for (ActivityResources activityResources : mActivityResourceReferences.values()) {
+ final int resCount = activityResources.activityResources.size();
+ for (int i = 0; i < resCount; i++) {
+ final WeakReference<Resources> ref = activityResources.activityResources.get(i);
+ final Resources r = ref != null ? ref.get() : null;
+ if (r != null) {
+ final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
+ if (key != null) {
+ final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
+ if (impl == null) {
+ throw new Resources.NotFoundException(
+ "failed to redirect ResourcesImpl");
+ }
+ r.setImpl(impl);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/ResultInfo.java b/android/app/ResultInfo.java
new file mode 100644
index 00000000..5e0867c3
--- /dev/null
+++ b/android/app/ResultInfo.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * {@hide}
+ */
+public class ResultInfo implements Parcelable {
+ public final String mResultWho;
+ public final int mRequestCode;
+ public final int mResultCode;
+ public final Intent mData;
+
+ public ResultInfo(String resultWho, int requestCode, int resultCode,
+ Intent data) {
+ mResultWho = resultWho;
+ mRequestCode = requestCode;
+ mResultCode = resultCode;
+ mData = data;
+ }
+
+ public String toString() {
+ return "ResultInfo{who=" + mResultWho + ", request=" + mRequestCode
+ + ", result=" + mResultCode + ", data=" + mData + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mResultWho);
+ out.writeInt(mRequestCode);
+ out.writeInt(mResultCode);
+ if (mData != null) {
+ out.writeInt(1);
+ mData.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Parcelable.Creator<ResultInfo> CREATOR
+ = new Parcelable.Creator<ResultInfo>() {
+ public ResultInfo createFromParcel(Parcel in) {
+ return new ResultInfo(in);
+ }
+
+ public ResultInfo[] newArray(int size) {
+ return new ResultInfo[size];
+ }
+ };
+
+ public ResultInfo(Parcel in) {
+ mResultWho = in.readString();
+ mRequestCode = in.readInt();
+ mResultCode = in.readInt();
+ if (in.readInt() != 0) {
+ mData = Intent.CREATOR.createFromParcel(in);
+ } else {
+ mData = null;
+ }
+ }
+}
diff --git a/android/app/SearchDialog.java b/android/app/SearchDialog.java
new file mode 100644
index 00000000..4abca9a1
--- /dev/null
+++ b/android/app/SearchDialog.java
@@ -0,0 +1,708 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.speech.RecognizerIntent;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AutoCompleteTextView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SearchView;
+import android.widget.TextView;
+
+/**
+ * Search dialog. This is controlled by the
+ * SearchManager and runs in the current foreground process.
+ *
+ * @hide
+ */
+public class SearchDialog extends Dialog {
+
+ // Debugging support
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SearchDialog";
+
+ private static final String INSTANCE_KEY_COMPONENT = "comp";
+ private static final String INSTANCE_KEY_APPDATA = "data";
+ private static final String INSTANCE_KEY_USER_QUERY = "uQry";
+
+ // The string used for privateImeOptions to identify to the IME that it should not show
+ // a microphone button since one already exists in the search dialog.
+ private static final String IME_OPTION_NO_MICROPHONE = "nm";
+
+ private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
+
+ // views & widgets
+ private TextView mBadgeLabel;
+ private ImageView mAppIcon;
+ private AutoCompleteTextView mSearchAutoComplete;
+ private View mSearchPlate;
+ private SearchView mSearchView;
+ private Drawable mWorkingSpinner;
+ private View mCloseSearch;
+
+ // interaction with searchable application
+ private SearchableInfo mSearchable;
+ private ComponentName mLaunchComponent;
+ private Bundle mAppSearchData;
+ private Context mActivityContext;
+
+ // For voice searching
+ private final Intent mVoiceWebSearchIntent;
+ private final Intent mVoiceAppSearchIntent;
+
+ // The query entered by the user. This is not changed when selecting a suggestion
+ // that modifies the contents of the text field. But if the user then edits
+ // the suggestion, the resulting string is saved.
+ private String mUserQuery;
+
+ // Last known IME options value for the search edit text.
+ private int mSearchAutoCompleteImeOptions;
+
+ private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ onConfigurationChanged();
+ }
+ }
+ };
+
+ static int resolveDialogTheme(Context context) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(com.android.internal.R.attr.searchDialogTheme,
+ outValue, true);
+ return outValue.resourceId;
+ }
+
+ /**
+ * Constructor - fires it up and makes it look like the search UI.
+ *
+ * @param context Application Context we can use for system acess
+ */
+ public SearchDialog(Context context, SearchManager searchManager) {
+ super(context, resolveDialogTheme(context));
+
+ // Save voice intent for later queries/launching
+ mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+ mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+
+ mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+
+ /**
+ * Create the search dialog and any resources that are used for the
+ * entire lifetime of the dialog.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Window theWindow = getWindow();
+ WindowManager.LayoutParams lp = theWindow.getAttributes();
+ lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ // taking up the whole window (even when transparent) is less than ideal,
+ // but necessary to show the popup window until the window manager supports
+ // having windows anchored by their parent but not clipped by them.
+ lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
+ lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ theWindow.setAttributes(lp);
+
+ // Touching outside of the search dialog will dismiss it
+ setCanceledOnTouchOutside(true);
+ }
+
+ /**
+ * We recreate the dialog view each time it becomes visible so as to limit
+ * the scope of any problems with the contained resources.
+ */
+ private void createContentView() {
+ setContentView(com.android.internal.R.layout.search_bar);
+
+ // get the view elements for local access
+ mSearchView = findViewById(com.android.internal.R.id.search_view);
+ mSearchView.setIconified(false);
+ mSearchView.setOnCloseListener(mOnCloseListener);
+ mSearchView.setOnQueryTextListener(mOnQueryChangeListener);
+ mSearchView.setOnSuggestionListener(mOnSuggestionSelectionListener);
+ mSearchView.onActionViewExpanded();
+
+ mCloseSearch = findViewById(com.android.internal.R.id.closeButton);
+ mCloseSearch.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+
+ // TODO: Move the badge logic to SearchView or move the badge to search_bar.xml
+ mBadgeLabel = (TextView) mSearchView.findViewById(com.android.internal.R.id.search_badge);
+ mSearchAutoComplete = (AutoCompleteTextView)
+ mSearchView.findViewById(com.android.internal.R.id.search_src_text);
+ mAppIcon = findViewById(com.android.internal.R.id.search_app_icon);
+ mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate);
+ mWorkingSpinner = getContext().getDrawable(com.android.internal.R.drawable.search_spinner);
+ // TODO: Restore the spinner for slow suggestion lookups
+ // mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
+ // null, null, mWorkingSpinner, null);
+ setWorking(false);
+
+ // pre-hide all the extraneous elements
+ mBadgeLabel.setVisibility(View.GONE);
+
+ // Additional adjustments to make Dialog work for Search
+ mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
+ }
+
+ /**
+ * Set up the search dialog
+ *
+ * @return true if search dialog launched, false if not
+ */
+ public boolean show(String initialQuery, boolean selectInitialQuery,
+ ComponentName componentName, Bundle appSearchData) {
+ boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData);
+ if (success) {
+ // Display the drop down as soon as possible instead of waiting for the rest of the
+ // pending UI stuff to get done, so that things appear faster to the user.
+ mSearchAutoComplete.showDropDownAfterLayout();
+ }
+ return success;
+ }
+
+ /**
+ * Does the rest of the work required to show the search dialog. Called by
+ * {@link #show(String, boolean, ComponentName, Bundle)} and
+ *
+ * @return true if search dialog showed, false if not
+ */
+ private boolean doShow(String initialQuery, boolean selectInitialQuery,
+ ComponentName componentName, Bundle appSearchData) {
+ // set up the searchable and show the dialog
+ if (!show(componentName, appSearchData)) {
+ return false;
+ }
+
+ // finally, load the user's initial text (which may trigger suggestions)
+ setUserQuery(initialQuery);
+ if (selectInitialQuery) {
+ mSearchAutoComplete.selectAll();
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets up the search dialog and shows it.
+ *
+ * @return <code>true</code> if search dialog launched
+ */
+ private boolean show(ComponentName componentName, Bundle appSearchData) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "show(" + componentName + ", "
+ + appSearchData + ")");
+ }
+
+ SearchManager searchManager = (SearchManager)
+ mContext.getSystemService(Context.SEARCH_SERVICE);
+ // Try to get the searchable info for the provided component.
+ mSearchable = searchManager.getSearchableInfo(componentName);
+
+ if (mSearchable == null) {
+ return false;
+ }
+
+ mLaunchComponent = componentName;
+ mAppSearchData = appSearchData;
+ mActivityContext = mSearchable.getActivityContext(getContext());
+
+ // show the dialog. this will call onStart().
+ if (!isShowing()) {
+ // Recreate the search bar view every time the dialog is shown, to get rid
+ // of any bad state in the AutoCompleteTextView etc
+ createContentView();
+ mSearchView.setSearchableInfo(mSearchable);
+ mSearchView.setAppSearchData(mAppSearchData);
+
+ show();
+ }
+ updateUI();
+
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Register a listener for configuration change events.
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ getContext().registerReceiver(mConfChangeListener, filter);
+ }
+
+ /**
+ * The search dialog is being dismissed, so handle all of the local shutdown operations.
+ *
+ * This function is designed to be idempotent so that dismiss() can be safely called at any time
+ * (even if already closed) and more likely to really dump any memory. No leaks!
+ */
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ getContext().unregisterReceiver(mConfChangeListener);
+
+ // dump extra memory we're hanging on to
+ mLaunchComponent = null;
+ mAppSearchData = null;
+ mSearchable = null;
+ mUserQuery = null;
+ }
+
+ /**
+ * Sets the search dialog to the 'working' state, which shows a working spinner in the
+ * right hand size of the text field.
+ *
+ * @param working true to show spinner, false to hide spinner
+ */
+ public void setWorking(boolean working) {
+ mWorkingSpinner.setAlpha(working ? 255 : 0);
+ mWorkingSpinner.setVisible(working, false);
+ mWorkingSpinner.invalidateSelf();
+ }
+
+ /**
+ * Save the minimal set of data necessary to recreate the search
+ *
+ * @return A bundle with the state of the dialog, or {@code null} if the search
+ * dialog is not showing.
+ */
+ @Override
+ public Bundle onSaveInstanceState() {
+ if (!isShowing()) return null;
+
+ Bundle bundle = new Bundle();
+
+ // setup info so I can recreate this particular search
+ bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
+ bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
+ bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
+
+ return bundle;
+ }
+
+ /**
+ * Restore the state of the dialog from a previously saved bundle.
+ *
+ * @param savedInstanceState The state of the dialog previously saved by
+ * {@link #onSaveInstanceState()}.
+ */
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (savedInstanceState == null) return;
+
+ ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
+ Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
+ String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
+
+ // show the dialog.
+ if (!doShow(userQuery, false, launchComponent, appSearchData)) {
+ // for some reason, we couldn't re-instantiate
+ return;
+ }
+ }
+
+ /**
+ * Called after resources have changed, e.g. after screen rotation or locale change.
+ */
+ public void onConfigurationChanged() {
+ if (mSearchable != null && isShowing()) {
+ // Redraw (resources may have changed)
+ updateSearchAppIcon();
+ updateSearchBadge();
+ if (isLandscapeMode(getContext())) {
+ mSearchAutoComplete.ensureImeVisible(true);
+ }
+ }
+ }
+
+ static boolean isLandscapeMode(Context context) {
+ return context.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ /**
+ * Update the UI according to the info in the current value of {@link #mSearchable}.
+ */
+ private void updateUI() {
+ if (mSearchable != null) {
+ mDecor.setVisibility(View.VISIBLE);
+ updateSearchAutoComplete();
+ updateSearchAppIcon();
+ updateSearchBadge();
+
+ // In order to properly configure the input method (if one is being used), we
+ // need to let it know if we'll be providing suggestions. Although it would be
+ // difficult/expensive to know if every last detail has been configured properly, we
+ // can at least see if a suggestions provider has been configured, and use that
+ // as our trigger.
+ int inputType = mSearchable.getInputType();
+ // We only touch this if the input type is set up for text (which it almost certainly
+ // should be, in the case of search!)
+ if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+ // The existence of a suggestions authority is the proxy for "suggestions
+ // are available here"
+ inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ if (mSearchable.getSuggestAuthority() != null) {
+ inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ }
+ }
+ mSearchAutoComplete.setInputType(inputType);
+ mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
+ mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
+
+ // If the search dialog is going to show a voice search button, then don't let
+ // the soft keyboard display a microphone button if it would have otherwise.
+ if (mSearchable.getVoiceSearchEnabled()) {
+ mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
+ } else {
+ mSearchAutoComplete.setPrivateImeOptions(null);
+ }
+ }
+ }
+
+ /**
+ * Updates the auto-complete text view.
+ */
+ private void updateSearchAutoComplete() {
+ // we dismiss the entire dialog instead
+ mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
+ mSearchAutoComplete.setForceIgnoreOutsideTouch(false);
+ }
+
+ private void updateSearchAppIcon() {
+ PackageManager pm = getContext().getPackageManager();
+ Drawable icon;
+ try {
+ ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
+ icon = pm.getApplicationIcon(info.applicationInfo);
+ if (DBG)
+ Log.d(LOG_TAG, "Using app-specific icon");
+ } catch (NameNotFoundException e) {
+ icon = pm.getDefaultActivityIcon();
+ Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
+ }
+ mAppIcon.setImageDrawable(icon);
+ mAppIcon.setVisibility(View.VISIBLE);
+ mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), mSearchPlate.getPaddingBottom());
+ }
+
+ /**
+ * Setup the search "Badge" if requested by mode flags.
+ */
+ private void updateSearchBadge() {
+ // assume both hidden
+ int visibility = View.GONE;
+ Drawable icon = null;
+ CharSequence text = null;
+
+ // optionally show one or the other.
+ if (mSearchable.useBadgeIcon()) {
+ icon = mActivityContext.getDrawable(mSearchable.getIconId());
+ visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
+ } else if (mSearchable.useBadgeLabel()) {
+ text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
+ visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
+ }
+
+ mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+ mBadgeLabel.setText(text);
+ mBadgeLabel.setVisibility(visibility);
+ }
+
+ /*
+ * Listeners of various types
+ */
+
+ /**
+ * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
+ * touch is outside the window. But the window includes space for the drop-down,
+ * so we also cancel on taps outside the search bar when the drop-down is not showing.
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // cancel if the drop-down is not showing and the touch event was outside the search plate
+ if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
+ if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
+ cancel();
+ return true;
+ }
+ // Let Dialog handle events outside the window while the pop-up is showing.
+ return super.onTouchEvent(event);
+ }
+
+ private boolean isOutOfBounds(View v, MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
+ return (x < -slop) || (y < -slop)
+ || (x > (v.getWidth()+slop))
+ || (y > (v.getHeight()+slop));
+ }
+
+ @Override
+ public void hide() {
+ if (!isShowing()) return;
+
+ // We made sure the IME was displayed, so also make sure it is closed
+ // when we go away.
+ InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(
+ getWindow().getDecorView().getWindowToken(), 0);
+ }
+
+ super.hide();
+ }
+
+ /**
+ * Launch a search for the text in the query text field.
+ */
+ public void launchQuerySearch() {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+
+ /**
+ * Launch a search for the text in the query text field.
+ *
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or <code>null</code> if none.
+ */
+ protected void launchQuerySearch(int actionKey, String actionMsg) {
+ String query = mSearchAutoComplete.getText().toString();
+ String action = Intent.ACTION_SEARCH;
+ Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
+ launchIntent(intent);
+ }
+
+ /**
+ * Launches an intent, including any special intent handling.
+ */
+ private void launchIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ Log.d(LOG_TAG, "launching " + intent);
+ try {
+ // If the intent was created from a suggestion, it will always have an explicit
+ // component here.
+ getContext().startActivity(intent);
+ // If the search switches to a different activity,
+ // SearchDialogWrapper#performActivityResuming
+ // will handle hiding the dialog when the next activity starts, but for
+ // real in-app search, we still need to dismiss the dialog.
+ dismiss();
+ } catch (RuntimeException ex) {
+ Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
+ }
+ }
+
+ /**
+ * Sets the list item selection in the AutoCompleteTextView's ListView.
+ */
+ public void setListSelection(int index) {
+ mSearchAutoComplete.setListSelection(index);
+ }
+
+ /**
+ * Constructs an intent from the given information and the search dialog state.
+ *
+ * @param action Intent action.
+ * @param data Intent data, or <code>null</code>.
+ * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
+ * @param query Intent query, or <code>null</code>.
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or <code>null</code> if none.
+ * @param mode The search mode, one of the acceptable values for
+ * {@link SearchManager#SEARCH_MODE}, or {@code null}.
+ * @return The intent.
+ */
+ private Intent createIntent(String action, Uri data, String extraData, String query,
+ int actionKey, String actionMsg) {
+ // Now build the Intent
+ Intent intent = new Intent(action);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // We need CLEAR_TOP to avoid reusing an old task that has other activities
+ // on top of the one we want. We don't want to do this in in-app search though,
+ // as it can be destructive to the activity stack.
+ if (data != null) {
+ intent.setData(data);
+ }
+ intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
+ if (query != null) {
+ intent.putExtra(SearchManager.QUERY, query);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ if (mAppSearchData != null) {
+ intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+ }
+ if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+ intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+ intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ }
+ intent.setComponent(mSearchable.getSearchActivity());
+ return intent;
+ }
+
+ /**
+ * The root element in the search bar layout. This is a custom view just to override
+ * the handling of the back button.
+ */
+ public static class SearchBar extends LinearLayout {
+
+ public SearchBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SearchBar(Context context) {
+ super(context);
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(
+ View child, ActionMode.Callback callback, int type) {
+ // Disable Primary Action Modes in the SearchBar, as they overlap.
+ if (type != ActionMode.TYPE_PRIMARY) {
+ return super.startActionModeForChild(child, callback, type);
+ }
+ return null;
+ }
+ }
+
+ private boolean isEmpty(AutoCompleteTextView actv) {
+ return TextUtils.getTrimmedLength(actv.getText()) == 0;
+ }
+
+ @Override
+ public void onBackPressed() {
+ // If the input method is covering the search dialog completely,
+ // e.g. in landscape mode with no hard keyboard, dismiss just the input method
+ InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+ if (imm != null && imm.isFullscreenMode() &&
+ imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
+ return;
+ }
+ // Close search dialog
+ cancel();
+ }
+
+ private boolean onClosePressed() {
+ // Dismiss the dialog if close button is pressed when there's no query text
+ if (isEmpty(mSearchAutoComplete)) {
+ dismiss();
+ return true;
+ }
+
+ return false;
+ }
+
+ private final SearchView.OnCloseListener mOnCloseListener = new SearchView.OnCloseListener() {
+
+ public boolean onClose() {
+ return onClosePressed();
+ }
+ };
+
+ private final SearchView.OnQueryTextListener mOnQueryChangeListener =
+ new SearchView.OnQueryTextListener() {
+
+ public boolean onQueryTextSubmit(String query) {
+ dismiss();
+ return false;
+ }
+
+ public boolean onQueryTextChange(String newText) {
+ return false;
+ }
+ };
+
+ private final SearchView.OnSuggestionListener mOnSuggestionSelectionListener =
+ new SearchView.OnSuggestionListener() {
+
+ public boolean onSuggestionSelect(int position) {
+ return false;
+ }
+
+ public boolean onSuggestionClick(int position) {
+ dismiss();
+ return false;
+ }
+ };
+
+ /**
+ * Sets the text in the query box, updating the suggestions.
+ */
+ private void setUserQuery(String query) {
+ if (query == null) {
+ query = "";
+ }
+ mUserQuery = query;
+ mSearchAutoComplete.setText(query);
+ mSearchAutoComplete.setSelection(query.length());
+ }
+}
diff --git a/android/app/SearchManager.java b/android/app/SearchManager.java
new file mode 100644
index 00000000..ea990ad2
--- /dev/null
+++ b/android/app/SearchManager.java
@@ -0,0 +1,993 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.SystemService;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.util.List;
+
+/**
+ * This class provides access to the system search services.
+ *
+ * <p>In practice, you won't interact with this class directly, as search
+ * services are provided through methods in {@link android.app.Activity Activity}
+ * and the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}
+ * {@link android.content.Intent Intent}.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using the search dialog and adding search
+ * suggestions in your application, read the
+ * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
+ * </div>
+ */
+@SystemService(Context.SEARCH_SERVICE)
+public class SearchManager
+ implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener {
+
+ private static final boolean DBG = false;
+ private static final String TAG = "SearchManager";
+
+ /**
+ * This is a shortcut definition for the default menu key to use for invoking search.
+ *
+ * See Menu.Item.setAlphabeticShortcut() for more information.
+ */
+ public final static char MENU_KEY = 's';
+
+ /**
+ * This is a shortcut definition for the default menu key to use for invoking search.
+ *
+ * See Menu.Item.setAlphabeticShortcut() for more information.
+ */
+ public final static int MENU_KEYCODE = KeyEvent.KEYCODE_S;
+
+ /**
+ * Intent extra data key: Use this key with
+ * {@link android.content.Intent#getStringExtra
+ * content.Intent.getStringExtra()}
+ * to obtain the query string from Intent.ACTION_SEARCH.
+ */
+ public final static String QUERY = "query";
+
+ /**
+ * Intent extra data key: Use this key with
+ * {@link android.content.Intent#getStringExtra
+ * content.Intent.getStringExtra()}
+ * to obtain the query string typed in by the user.
+ * This may be different from the value of {@link #QUERY}
+ * if the intent is the result of selecting a suggestion.
+ * In that case, {@link #QUERY} will contain the value of
+ * {@link #SUGGEST_COLUMN_QUERY} for the suggestion, and
+ * {@link #USER_QUERY} will contain the string typed by the
+ * user.
+ */
+ public final static String USER_QUERY = "user_query";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getBundleExtra
+ * content.Intent.getBundleExtra()}
+ * to obtain any additional app-specific data that was inserted by the
+ * activity that launched the search.
+ */
+ public final static String APP_DATA = "app_data";
+
+ /**
+ * Intent extra data key: Use {@link android.content.Intent#getBundleExtra
+ * content.Intent.getBundleExtra(SEARCH_MODE)} to get the search mode used
+ * to launch the intent.
+ * The only current value for this is {@link #MODE_GLOBAL_SEARCH_SUGGESTION}.
+ *
+ * @hide
+ */
+ public final static String SEARCH_MODE = "search_mode";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getIntExtra content.Intent.getIntExtra()}
+ * to obtain the keycode that the user used to trigger this query. It will be zero if the
+ * user simply pressed the "GO" button on the search UI. This is primarily used in conjunction
+ * with the keycode attribute in the actionkey element of your searchable.xml configuration
+ * file.
+ */
+ public final static String ACTION_KEY = "action_key";
+
+ /**
+ * Intent extra data key: This key will be used for the extra populated by the
+ * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column.
+ */
+ public final static String EXTRA_DATA_KEY = "intent_extra_data_key";
+
+ /**
+ * Boolean extra data key for {@link #INTENT_ACTION_GLOBAL_SEARCH} intents. If {@code true},
+ * the initial query should be selected when the global search activity is started, so
+ * that the user can easily replace it with another query.
+ */
+ public final static String EXTRA_SELECT_QUERY = "select_query";
+
+ /**
+ * Boolean extra data key for {@link Intent#ACTION_WEB_SEARCH} intents. If {@code true},
+ * this search should open a new browser window, rather than using an existing one.
+ */
+ public final static String EXTRA_NEW_SEARCH = "new_search";
+
+ /**
+ * Extra data key for {@link Intent#ACTION_WEB_SEARCH}. If set, the value must be a
+ * {@link PendingIntent}. The search activity handling the {@link Intent#ACTION_WEB_SEARCH}
+ * intent will fill in and launch the pending intent. The data URI will be filled in with an
+ * http or https URI, and {@link android.provider.Browser#EXTRA_HEADERS} may be filled in.
+ */
+ public static final String EXTRA_WEB_SEARCH_PENDINGINTENT = "web_search_pendingintent";
+
+ /**
+ * Boolean extra data key for a suggestion provider to return in {@link Cursor#getExtras} to
+ * indicate that the search is not complete yet. This can be used by the search UI
+ * to indicate that a search is in progress. The suggestion provider can return partial results
+ * this way and send a change notification on the cursor when more results are available.
+ */
+ public final static String CURSOR_EXTRA_KEY_IN_PROGRESS = "in_progress";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
+ * to obtain the action message that was defined for a particular search action key and/or
+ * suggestion. It will be null if the search was launched by typing "enter", touched the the
+ * "GO" button, or other means not involving any action key.
+ */
+ public final static String ACTION_MSG = "action_msg";
+
+ /**
+ * Flag to specify that the entry can be used for query refinement, i.e., the query text
+ * in the search field can be replaced with the text in this entry, when a query refinement
+ * icon is clicked. The suggestion list should show such a clickable icon beside the entry.
+ * <p>Use this flag as a bit-field for {@link #SUGGEST_COLUMN_FLAGS}.
+ */
+ public final static int FLAG_QUERY_REFINEMENT = 1 << 0;
+
+ /**
+ * Uri path for queried suggestions data. This is the path that the search manager
+ * will use when querying your content provider for suggestions data based on user input
+ * (e.g. looking for partial matches).
+ * Typically you'll use this with a URI matcher.
+ */
+ public final static String SUGGEST_URI_PATH_QUERY = "search_suggest_query";
+
+ /**
+ * MIME type for suggestions data. You'll use this in your suggestions content provider
+ * in the getType() function.
+ */
+ public final static String SUGGEST_MIME_TYPE =
+ "vnd.android.cursor.dir/vnd.android.search.suggest";
+
+ /**
+ * Uri path for shortcut validation. This is the path that the search manager will use when
+ * querying your content provider to refresh a shortcutted suggestion result and to check if it
+ * is still valid. When asked, a source may return an up to date result, or no result. No
+ * result indicates the shortcut refers to a no longer valid sugggestion.
+ *
+ * @see #SUGGEST_COLUMN_SHORTCUT_ID
+ */
+ public final static String SUGGEST_URI_PATH_SHORTCUT = "search_suggest_shortcut";
+
+ /**
+ * MIME type for shortcut validation. You'll use this in your suggestions content provider
+ * in the getType() function.
+ */
+ public final static String SHORTCUT_MIME_TYPE =
+ "vnd.android.cursor.item/vnd.android.search.suggest";
+
+ /**
+ * Column name for suggestions cursor. <i>Unused - can be null or column can be omitted.</i>
+ */
+ public final static String SUGGEST_COLUMN_FORMAT = "suggest_format";
+ /**
+ * Column name for suggestions cursor. <i>Required.</i> This is the primary line of text that
+ * will be presented to the user as the suggestion.
+ */
+ public final static String SUGGEST_COLUMN_TEXT_1 = "suggest_text_1";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a two-line format. The second line of text is in
+ * a much smaller appearance.
+ */
+ public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> This is a URL that will be shown
+ * as the second line of text instead of {@link #SUGGEST_COLUMN_TEXT_2}. This is a separate
+ * column so that the search UI knows to display the text as a URL, e.g. by using a different
+ * color. If this column is absent, or has the value {@code null},
+ * {@link #SUGGEST_COLUMN_TEXT_2} will be used instead.
+ */
+ public final static String SUGGEST_COLUMN_TEXT_2_URL = "suggest_text_2_url";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a resource ID of a drawable, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
+ * for more information on these schemes.
+ */
+ public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a resource ID of a drawable, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
+ * for more information on these schemes.
+ */
+ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then the image will be displayed when forming the suggestion. The suggested dimension for
+ * the image is 270x400 px for portrait mode and 400x225 px for landscape mode. The data in the
+ * column must be a resource ID of a drawable, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
+ * for more information on these schemes.
+ */
+ public final static String SUGGEST_COLUMN_RESULT_CARD_IMAGE = "suggest_result_card_image";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, this is the action that will be used when
+ * forming the suggestion's intent. If the element is not provided, the action will be taken
+ * from the android:searchSuggestIntentAction field in your XML metadata. <i>At least one of
+ * these must be present for the suggestion to generate an intent.</i> Note: If your action is
+ * the same for all suggestions, it is more efficient to specify it using XML metadata and omit
+ * it from the cursor.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_ACTION = "suggest_intent_action";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, this is the data that will be used when
+ * forming the suggestion's intent. If the element is not provided, the data will be taken
+ * from the android:searchSuggestIntentData field in your XML metadata. If neither source
+ * is provided, the Intent's data field will be null. Note: If your data is
+ * the same for all suggestions, or can be described using a constant part and a specific ID,
+ * it is more efficient to specify it using XML metadata and omit it from the cursor.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, this is the data that will be used when
+ * forming the suggestion's intent. If not provided, the Intent's extra data field will be null.
+ * This column allows suggestions to provide additional arbitrary data which will be included as
+ * an extra under the key {@link #EXTRA_DATA_KEY}.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, then "/" and this value will be appended to the data
+ * field in the Intent. This should only be used if the data field has already been set to an
+ * appropriate base string.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_DATA_ID = "suggest_intent_data_id";
+
+ /**
+ * Column name for suggestions cursor. <i>Required if action is
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}, optional otherwise.</i> If this
+ * column exists <i>and</i> this element exists at the given row, this is the data that will be
+ * used when forming the suggestion's query.
+ */
+ public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> This column is used to indicate whether
+ * a search suggestion should be stored as a shortcut, and whether it should be refreshed. If
+ * missing, the result will be stored as a shortcut and never validated. If set to
+ * {@link #SUGGEST_NEVER_MAKE_SHORTCUT}, the result will not be stored as a shortcut.
+ * Otherwise, the shortcut id will be used to check back for an up to date suggestion using
+ * {@link #SUGGEST_URI_PATH_SHORTCUT}.
+ */
+ public final static String SUGGEST_COLUMN_SHORTCUT_ID = "suggest_shortcut_id";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> This column is used to specify
+ * that a spinner should be shown in lieu of an icon2 while the shortcut of this suggestion
+ * is being refreshed.
+ */
+ public final static String SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING =
+ "suggest_spinner_while_refreshing";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is media type, you
+ * should provide this column so search app could understand more about your content. The data
+ * in the column must specify the MIME type of the content.
+ */
+ public final static String SUGGEST_COLUMN_CONTENT_TYPE = "suggest_content_type";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is media type, you
+ * should provide this column to specify whether your content is live media such as live video
+ * or live audio. The value in the column is of integer type with value of either 0 indicating
+ * non-live content or 1 indicating live content.
+ */
+ public final static String SUGGEST_COLUMN_IS_LIVE = "suggest_is_live";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is video, you should
+ * provide this column to specify the number of vertical lines. The data in the column is of
+ * integer type.
+ */
+ public final static String SUGGEST_COLUMN_VIDEO_WIDTH = "suggest_video_width";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is video, you should
+ * provide this column to specify the number of horizontal lines. The data in the column is of
+ * integer type.
+ */
+ public final static String SUGGEST_COLUMN_VIDEO_HEIGHT = "suggest_video_height";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content contains audio, you
+ * should provide this column to specify the audio channel configuration. The data in the
+ * column is string with format like "channels.subchannels" such as "1.0" or "5.1".
+ */
+ public final static String SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG = "suggest_audio_channel_config";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is purchasable, you
+ * should provide this column to specify the displayable string representation of the purchase
+ * price of your content including the currency and the amount. If it's free, you should
+ * provide localized string to specify that it's free. This column can be omitted if the content
+ * is not applicable to purchase.
+ */
+ public final static String SUGGEST_COLUMN_PURCHASE_PRICE = "suggest_purchase_price";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is rentable, you
+ * should provide this column to specify the displayable string representation of the rental
+ * price of your content including the currency and the amount. If it's free, you should
+ * provide localized string to specify that it's free. This column can be ommitted if the
+ * content is not applicable to rent.
+ */
+ public final static String SUGGEST_COLUMN_RENTAL_PRICE = "suggest_rental_price";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content has a rating, you
+ * should provide this column to specify the rating style of your content. The data in the
+ * column must be one of the constant values specified in {@link android.media.Rating}
+ */
+ public final static String SUGGEST_COLUMN_RATING_STYLE = "suggest_rating_style";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content has a rating, you
+ * should provide this column to specify the rating score of your content. The data in the
+ * column is of float type. See {@link android.media.Rating} about valid rating scores for each
+ * rating style.
+ */
+ public final static String SUGGEST_COLUMN_RATING_SCORE = "suggest_rating_score";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is video or audio and
+ * has a known production year, you should provide this column to specify the production year
+ * of your content. The data in the column is of integer type.
+ */
+ public final static String SUGGEST_COLUMN_PRODUCTION_YEAR = "suggest_production_year";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your content is video or audio, you
+ * should provide this column to specify the duration of your content in milliseconds. The data
+ * in the column is of long type.
+ */
+ public final static String SUGGEST_COLUMN_DURATION = "suggest_duration";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> This column is used to specify
+ * additional flags per item. Multiple flags can be specified.
+ * <p>
+ * Must be one of {@link #FLAG_QUERY_REFINEMENT} or 0 to indicate no flags.
+ * </p>
+ */
+ public final static String SUGGEST_COLUMN_FLAGS = "suggest_flags";
+
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> This column may be
+ * used to specify the time in {@link System#currentTimeMillis
+ * System.currentTImeMillis()} (wall time in UTC) when an item was last
+ * accessed within the results-providing application. If set, this may be
+ * used to show more-recently-used items first.
+ */
+ public final static String SUGGEST_COLUMN_LAST_ACCESS_HINT = "suggest_last_access_hint";
+
+ /**
+ * Column value for suggestion column {@link #SUGGEST_COLUMN_SHORTCUT_ID} when a suggestion
+ * should not be stored as a shortcut in global search.
+ */
+ public final static String SUGGEST_NEVER_MAKE_SHORTCUT = "_-1";
+
+ /**
+ * Query parameter added to suggestion queries to limit the number of suggestions returned.
+ * This limit is only advisory and suggestion providers may chose to ignore it.
+ */
+ public final static String SUGGEST_PARAMETER_LIMIT = "limit";
+
+ /**
+ * Intent action for starting the global search activity.
+ * The global search provider should handle this intent.
+ *
+ * Supported extra data keys: {@link #QUERY},
+ * {@link #EXTRA_SELECT_QUERY},
+ * {@link #APP_DATA}.
+ */
+ public final static String INTENT_ACTION_GLOBAL_SEARCH
+ = "android.search.action.GLOBAL_SEARCH";
+
+ /**
+ * Intent action for starting the global search settings activity.
+ * The global search provider should handle this intent.
+ */
+ public final static String INTENT_ACTION_SEARCH_SETTINGS
+ = "android.search.action.SEARCH_SETTINGS";
+
+ /**
+ * Intent action for starting a web search provider's settings activity.
+ * Web search providers should handle this intent if they have provider-specific
+ * settings to implement.
+ */
+ public final static String INTENT_ACTION_WEB_SEARCH_SETTINGS
+ = "android.search.action.WEB_SEARCH_SETTINGS";
+
+ /**
+ * Intent action broadcasted to inform that the searchables list or default have changed.
+ * Components should handle this intent if they cache any searchable data and wish to stay
+ * up to date on changes.
+ */
+ public final static String INTENT_ACTION_SEARCHABLES_CHANGED
+ = "android.search.action.SEARCHABLES_CHANGED";
+
+ /**
+ * Intent action to be broadcast to inform that the global search provider
+ * has changed.
+ */
+ public final static String INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED
+ = "android.search.action.GLOBAL_SEARCH_ACTIVITY_CHANGED";
+
+ /**
+ * Intent action broadcasted to inform that the search settings have changed in some way.
+ * Either searchables have been enabled or disabled, or a different web search provider
+ * has been chosen.
+ */
+ public final static String INTENT_ACTION_SEARCH_SETTINGS_CHANGED
+ = "android.search.action.SETTINGS_CHANGED";
+
+ /**
+ * This means that context is voice, and therefore the SearchDialog should
+ * continue showing the microphone until the user indicates that he/she does
+ * not want to re-speak (e.g. by typing).
+ *
+ * @hide
+ */
+ public final static String CONTEXT_IS_VOICE = "android.search.CONTEXT_IS_VOICE";
+
+ /**
+ * This means that the voice icon should not be shown at all, because the
+ * current search engine does not support voice search.
+ * @hide
+ */
+ public final static String DISABLE_VOICE_SEARCH
+ = "android.search.DISABLE_VOICE_SEARCH";
+
+ /**
+ * Reference to the shared system search service.
+ */
+ private final ISearchManager mService;
+
+ private final Context mContext;
+
+ // package private since they are used by the inner class SearchManagerCallback
+ /* package */ final Handler mHandler;
+ /* package */ OnDismissListener mDismissListener = null;
+ /* package */ OnCancelListener mCancelListener = null;
+
+ private SearchDialog mSearchDialog;
+
+ /*package*/ SearchManager(Context context, Handler handler) throws ServiceNotFoundException {
+ mContext = context;
+ mHandler = handler;
+ mService = ISearchManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.SEARCH_SERVICE));
+ }
+
+ /**
+ * Launch search UI.
+ *
+ * <p>The search manager will open a search widget in an overlapping
+ * window, and the underlying activity may be obscured. The search
+ * entry state will remain in effect until one of the following events:
+ * <ul>
+ * <li>The user completes the search. In most cases this will launch
+ * a search intent.</li>
+ * <li>The user uses the back, home, or other keys to exit the search.</li>
+ * <li>The application calls the {@link #stopSearch}
+ * method, which will hide the search window and return focus to the
+ * activity from which it was launched.</li>
+ *
+ * <p>Most applications will <i>not</i> use this interface to invoke search.
+ * The primary method for invoking search is to call
+ * {@link android.app.Activity#onSearchRequested Activity.onSearchRequested()} or
+ * {@link android.app.Activity#startSearch Activity.startSearch()}.
+ *
+ * @param initialQuery A search string can be pre-entered here, but this
+ * is typically null or empty.
+ * @param selectInitialQuery If true, the intial query will be preselected, which means that
+ * any further typing will replace it. This is useful for cases where an entire pre-formed
+ * query is being inserted. If false, the selection point will be placed at the end of the
+ * inserted query. This is useful when the inserted query is text that the user entered,
+ * and the user would expect to be able to keep typing. <i>This parameter is only meaningful
+ * if initialQuery is a non-empty string.</i>
+ * @param launchActivity The ComponentName of the activity that has launched this search.
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ * @param globalSearch If false, this will only launch the search that has been specifically
+ * defined by the application (which is usually defined as a local search). If no default
+ * search is defined in the current application or activity, global search will be launched.
+ * If true, this will always launch a platform-global (e.g. web-based) search instead.
+ *
+ * @see android.app.Activity#onSearchRequested
+ * @see #stopSearch
+ */
+ public void startSearch(String initialQuery,
+ boolean selectInitialQuery,
+ ComponentName launchActivity,
+ Bundle appSearchData,
+ boolean globalSearch) {
+ startSearch(initialQuery, selectInitialQuery, launchActivity,
+ appSearchData, globalSearch, null);
+ }
+
+ /**
+ * As {@link #startSearch(String, boolean, ComponentName, Bundle, boolean)} but including
+ * source bounds for the global search intent.
+ *
+ * @hide
+ */
+ public void startSearch(String initialQuery,
+ boolean selectInitialQuery,
+ ComponentName launchActivity,
+ Bundle appSearchData,
+ boolean globalSearch,
+ Rect sourceBounds) {
+ if (globalSearch) {
+ startGlobalSearch(initialQuery, selectInitialQuery, appSearchData, sourceBounds);
+ return;
+ }
+
+ final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class);
+ // Don't show search dialog on televisions.
+ if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_TELEVISION) {
+ ensureSearchDialog();
+
+ mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData);
+ }
+ }
+
+ private void ensureSearchDialog() {
+ if (mSearchDialog == null) {
+ mSearchDialog = new SearchDialog(mContext, this);
+ mSearchDialog.setOnCancelListener(this);
+ mSearchDialog.setOnDismissListener(this);
+ }
+ }
+
+ /**
+ * Starts the global search activity.
+ */
+ /* package */ void startGlobalSearch(String initialQuery, boolean selectInitialQuery,
+ Bundle appSearchData, Rect sourceBounds) {
+ ComponentName globalSearchActivity = getGlobalSearchActivity();
+ if (globalSearchActivity == null) {
+ Log.w(TAG, "No global search activity found.");
+ return;
+ }
+ Intent intent = new Intent(INTENT_ACTION_GLOBAL_SEARCH);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setComponent(globalSearchActivity);
+ // Make sure that we have a Bundle to put source in
+ if (appSearchData == null) {
+ appSearchData = new Bundle();
+ } else {
+ appSearchData = new Bundle(appSearchData);
+ }
+ // Set source to package name of app that starts global search, if not set already.
+ if (!appSearchData.containsKey("source")) {
+ appSearchData.putString("source", mContext.getPackageName());
+ }
+ intent.putExtra(APP_DATA, appSearchData);
+ if (!TextUtils.isEmpty(initialQuery)) {
+ intent.putExtra(QUERY, initialQuery);
+ }
+ if (selectInitialQuery) {
+ intent.putExtra(EXTRA_SELECT_QUERY, selectInitialQuery);
+ }
+ intent.setSourceBounds(sourceBounds);
+ try {
+ if (DBG) Log.d(TAG, "Starting global search: " + intent.toUri(0));
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Log.e(TAG, "Global search activity not found: " + globalSearchActivity);
+ }
+ }
+
+ /**
+ * Returns a list of installed apps that handle the global search
+ * intent.
+ *
+ * @hide
+ */
+ public List<ResolveInfo> getGlobalSearchActivities() {
+ try {
+ return mService.getGlobalSearchActivities();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the name of the global search activity.
+ */
+ public ComponentName getGlobalSearchActivity() {
+ try {
+ return mService.getGlobalSearchActivity();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the name of the web search activity.
+ *
+ * @return The name of the default activity for web searches. This activity
+ * can be used to get web search suggestions. Returns {@code null} if
+ * there is no default web search activity.
+ *
+ * @hide
+ */
+ public ComponentName getWebSearchActivity() {
+ try {
+ return mService.getWebSearchActivity();
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Similar to {@link #startSearch} but actually fires off the search query after invoking
+ * the search dialog. Made available for testing purposes.
+ *
+ * @param query The query to trigger. If empty, request will be ignored.
+ * @param launchActivity The ComponentName of the activity that has launched this search.
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ *
+ * @see #startSearch
+ */
+ public void triggerSearch(String query,
+ ComponentName launchActivity,
+ Bundle appSearchData) {
+ if (query == null || TextUtils.getTrimmedLength(query) == 0) {
+ Log.w(TAG, "triggerSearch called with empty query, ignoring.");
+ return;
+ }
+ startSearch(query, false, launchActivity, appSearchData, false);
+ mSearchDialog.launchQuerySearch();
+ }
+
+ /**
+ * Terminate search UI.
+ *
+ * <p>Typically the user will terminate the search UI by launching a
+ * search or by canceling. This function allows the underlying application
+ * or activity to cancel the search prematurely (for any reason).
+ *
+ * <p>This function can be safely called at any time (even if no search is active.)
+ *
+ * @see #startSearch
+ */
+ public void stopSearch() {
+ if (mSearchDialog != null) {
+ mSearchDialog.cancel();
+ }
+ }
+
+ /**
+ * Determine if the Search UI is currently displayed.
+ *
+ * This is provided primarily for application test purposes.
+ *
+ * @return Returns true if the search UI is currently displayed.
+ *
+ * @hide
+ */
+ public boolean isVisible() {
+ return mSearchDialog == null? false : mSearchDialog.isShowing();
+ }
+
+ /**
+ * See {@link SearchManager#setOnDismissListener} for configuring your activity to monitor
+ * search UI state.
+ */
+ public interface OnDismissListener {
+ /**
+ * This method will be called when the search UI is dismissed. To make use of it, you must
+ * implement this method in your activity, and call
+ * {@link SearchManager#setOnDismissListener} to register it.
+ */
+ public void onDismiss();
+ }
+
+ /**
+ * See {@link SearchManager#setOnCancelListener} for configuring your activity to monitor
+ * search UI state.
+ */
+ public interface OnCancelListener {
+ /**
+ * This method will be called when the search UI is canceled. To make use if it, you must
+ * implement this method in your activity, and call
+ * {@link SearchManager#setOnCancelListener} to register it.
+ */
+ public void onCancel();
+ }
+
+ /**
+ * Set or clear the callback that will be invoked whenever the search UI is dismissed.
+ *
+ * @param listener The {@link OnDismissListener} to use, or null.
+ */
+ public void setOnDismissListener(final OnDismissListener listener) {
+ mDismissListener = listener;
+ }
+
+ /**
+ * Set or clear the callback that will be invoked whenever the search UI is canceled.
+ *
+ * @param listener The {@link OnCancelListener} to use, or null.
+ */
+ public void setOnCancelListener(OnCancelListener listener) {
+ mCancelListener = listener;
+ }
+
+ /**
+ * @deprecated This method is an obsolete internal implementation detail. Do not use.
+ */
+ @Deprecated
+ public void onCancel(DialogInterface dialog) {
+ if (mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+
+ /**
+ * @deprecated This method is an obsolete internal implementation detail. Do not use.
+ */
+ @Deprecated
+ public void onDismiss(DialogInterface dialog) {
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss();
+ }
+ }
+
+ /**
+ * Gets information about a searchable activity.
+ *
+ * @param componentName The activity to get searchable information for.
+ * @return Searchable information, or <code>null</code> if the activity does not
+ * exist, or is not searchable.
+ */
+ public SearchableInfo getSearchableInfo(ComponentName componentName) {
+ try {
+ return mService.getSearchableInfo(componentName);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets a cursor with search suggestions.
+ *
+ * @param searchable Information about how to get the suggestions.
+ * @param query The search text entered (so far).
+ * @return a cursor with suggestions, or <code>null</null> the suggestion query failed.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public Cursor getSuggestions(SearchableInfo searchable, String query) {
+ return getSuggestions(searchable, query, -1);
+ }
+
+ /**
+ * Gets a cursor with search suggestions.
+ *
+ * @param searchable Information about how to get the suggestions.
+ * @param query The search text entered (so far).
+ * @param limit The query limit to pass to the suggestion provider. This is advisory,
+ * the returned cursor may contain more rows. Pass {@code -1} for no limit.
+ * @return a cursor with suggestions, or <code>null</null> the suggestion query failed.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) {
+ if (searchable == null) {
+ return null;
+ }
+
+ String authority = searchable.getSuggestAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ Uri.Builder uriBuilder = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ .fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+
+ // if content path provided, insert it now
+ final String contentPath = searchable.getSuggestPath();
+ if (contentPath != null) {
+ uriBuilder.appendEncodedPath(contentPath);
+ }
+
+ // append standard suggestion query path
+ uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
+
+ // get the query selection, may be null
+ String selection = searchable.getSuggestSelection();
+ // inject query, either as selection args or inline
+ String[] selArgs = null;
+ if (selection != null) { // use selection if provided
+ selArgs = new String[] { query };
+ } else { // no selection, use REST pattern
+ uriBuilder.appendPath(query);
+ }
+
+ if (limit > 0) {
+ uriBuilder.appendQueryParameter(SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));
+ }
+
+ Uri uri = uriBuilder.build();
+
+ // finally, make the query
+ return mContext.getContentResolver().query(uri, null, selection, selArgs, null);
+ }
+
+ /**
+ * Returns a list of the searchable activities that can be included in global search.
+ *
+ * @return a list containing searchable information for all searchable activities
+ * that have the <code>android:includeInGlobalSearch</code> attribute set
+ * in their searchable meta-data.
+ */
+ public List<SearchableInfo> getSearchablesInGlobalSearch() {
+ try {
+ return mService.getSearchablesInGlobalSearch();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets an intent for launching installed assistant activity, or null if not available.
+ * @return The assist intent.
+ *
+ * @hide
+ */
+ public Intent getAssistIntent(boolean inclContext) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_ASSIST);
+ if (inclContext) {
+ IActivityManager am = ActivityManager.getService();
+ Bundle extras = am.getAssistContextExtras(ActivityManager.ASSIST_CONTEXT_BASIC);
+ if (extras != null) {
+ intent.replaceExtras(extras);
+ }
+ }
+ return intent;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts the assistant.
+ *
+ * @param args the args to pass to the assistant
+ *
+ * @hide
+ */
+ public void launchAssist(Bundle args) {
+ try {
+ if (mService == null) {
+ return;
+ }
+ mService.launchAssist(args);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts the legacy assistant (i.e. the {@link Intent#ACTION_ASSIST}).
+ *
+ * @param args the args to pass to the assistant
+ *
+ * @hide
+ */
+ public boolean launchLegacyAssist(String hint, int userHandle, Bundle args) {
+ try {
+ if (mService == null) {
+ return false;
+ }
+ return mService.launchLegacyAssist(hint, userHandle, args);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/app/SearchableInfo.java b/android/app/SearchableInfo.java
new file mode 100644
index 00000000..a9529151
--- /dev/null
+++ b/android/app/SearchableInfo.java
@@ -0,0 +1,887 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.StringRes;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import android.view.inputmethod.EditorInfo;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Searchability meta-data for an activity. Only applications that search other applications
+ * should need to use this class.
+ * See <a href="{@docRoot}guide/topics/search/searchable-config.html">Searchable Configuration</a>
+ * for more information about declaring searchability meta-data for your application.
+ *
+ * @see SearchManager#getSearchableInfo(ComponentName)
+ * @see SearchManager#getSearchablesInGlobalSearch()
+ */
+public final class SearchableInfo implements Parcelable {
+
+ // general debugging support
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SearchableInfo";
+
+ // static strings used for XML lookups.
+ // TODO how should these be documented for the developer, in a more structured way than
+ // the current long wordy javadoc in SearchManager.java ?
+ private static final String MD_LABEL_SEARCHABLE = "android.app.searchable";
+ private static final String MD_XML_ELEMENT_SEARCHABLE = "searchable";
+ private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey";
+
+ // flags in the searchMode attribute
+ private static final int SEARCH_MODE_BADGE_LABEL = 0x04;
+ private static final int SEARCH_MODE_BADGE_ICON = 0x08;
+ private static final int SEARCH_MODE_QUERY_REWRITE_FROM_DATA = 0x10;
+ private static final int SEARCH_MODE_QUERY_REWRITE_FROM_TEXT = 0x20;
+
+ // true member variables - what we know about the searchability
+ private final int mLabelId;
+ private final ComponentName mSearchActivity;
+ private final int mHintId;
+ private final int mSearchMode;
+ private final int mIconId;
+ private final int mSearchButtonText;
+ private final int mSearchInputType;
+ private final int mSearchImeOptions;
+ private final boolean mIncludeInGlobalSearch;
+ private final boolean mQueryAfterZeroResults;
+ private final boolean mAutoUrlDetect;
+ private final int mSettingsDescriptionId;
+ private final String mSuggestAuthority;
+ private final String mSuggestPath;
+ private final String mSuggestSelection;
+ private final String mSuggestIntentAction;
+ private final String mSuggestIntentData;
+ private final int mSuggestThreshold;
+ // Maps key codes to action key information. auto-boxing is not so bad here,
+ // since keycodes for the hard keys are < 127. For such values, Integer.valueOf()
+ // uses shared Integer objects.
+ // This is not final, to allow lazy initialization.
+ private HashMap<Integer,ActionKeyInfo> mActionKeys = null;
+ private final String mSuggestProviderPackage;
+
+ // Flag values for Searchable_voiceSearchMode
+ private static final int VOICE_SEARCH_SHOW_BUTTON = 1;
+ private static final int VOICE_SEARCH_LAUNCH_WEB_SEARCH = 2;
+ private static final int VOICE_SEARCH_LAUNCH_RECOGNIZER = 4;
+ private final int mVoiceSearchMode;
+ private final int mVoiceLanguageModeId; // voiceLanguageModel
+ private final int mVoicePromptTextId; // voicePromptText
+ private final int mVoiceLanguageId; // voiceLanguage
+ private final int mVoiceMaxResults; // voiceMaxResults
+
+ /**
+ * Gets the search suggestion content provider authority.
+ *
+ * @return The search suggestions authority, or {@code null} if not set.
+ * @see android.R.styleable#Searchable_searchSuggestAuthority
+ */
+ public String getSuggestAuthority() {
+ return mSuggestAuthority;
+ }
+
+ /**
+ * Gets the name of the package where the suggestion provider lives,
+ * or {@code null}.
+ */
+ public String getSuggestPackage() {
+ return mSuggestProviderPackage;
+ }
+
+ /**
+ * Gets the component name of the searchable activity.
+ *
+ * @return A component name, never {@code null}.
+ */
+ public ComponentName getSearchActivity() {
+ return mSearchActivity;
+ }
+
+ /**
+ * Checks whether the badge should be a text label.
+ *
+ * @see android.R.styleable#Searchable_searchMode
+ *
+ * @hide This feature is deprecated, no need to add it to the API.
+ */
+ public boolean useBadgeLabel() {
+ return 0 != (mSearchMode & SEARCH_MODE_BADGE_LABEL);
+ }
+
+ /**
+ * Checks whether the badge should be an icon.
+ *
+ * @see android.R.styleable#Searchable_searchMode
+ *
+ * @hide This feature is deprecated, no need to add it to the API.
+ */
+ public boolean useBadgeIcon() {
+ return (0 != (mSearchMode & SEARCH_MODE_BADGE_ICON)) && (mIconId != 0);
+ }
+
+ /**
+ * Checks whether the text in the query field should come from the suggestion intent data.
+ *
+ * @see android.R.styleable#Searchable_searchMode
+ */
+ public boolean shouldRewriteQueryFromData() {
+ return 0 != (mSearchMode & SEARCH_MODE_QUERY_REWRITE_FROM_DATA);
+ }
+
+ /**
+ * Checks whether the text in the query field should come from the suggestion title.
+ *
+ * @see android.R.styleable#Searchable_searchMode
+ */
+ public boolean shouldRewriteQueryFromText() {
+ return 0 != (mSearchMode & SEARCH_MODE_QUERY_REWRITE_FROM_TEXT);
+ }
+
+ /**
+ * Gets the resource id of the description string to use for this source in system search
+ * settings, or {@code 0} if none has been specified.
+ *
+ * @see android.R.styleable#Searchable_searchSettingsDescription
+ */
+ public int getSettingsDescriptionId() {
+ return mSettingsDescriptionId;
+ }
+
+ /**
+ * Gets the content provider path for obtaining search suggestions.
+ *
+ * @return The suggestion path, or {@code null} if not set.
+ * @see android.R.styleable#Searchable_searchSuggestPath
+ */
+ public String getSuggestPath() {
+ return mSuggestPath;
+ }
+
+ /**
+ * Gets the selection for obtaining search suggestions.
+ *
+ * @see android.R.styleable#Searchable_searchSuggestSelection
+ */
+ public String getSuggestSelection() {
+ return mSuggestSelection;
+ }
+
+ /**
+ * Gets the optional intent action for use with these suggestions. This is
+ * useful if all intents will have the same action
+ * (e.g. {@link android.content.Intent#ACTION_VIEW})
+ *
+ * This can be overriden in any given suggestion using the column
+ * {@link SearchManager#SUGGEST_COLUMN_INTENT_ACTION}.
+ *
+ * @return The default intent action, or {@code null} if not set.
+ * @see android.R.styleable#Searchable_searchSuggestIntentAction
+ */
+ public String getSuggestIntentAction() {
+ return mSuggestIntentAction;
+ }
+
+ /**
+ * Gets the optional intent data for use with these suggestions. This is
+ * useful if all intents will have similar data URIs,
+ * but you'll likely need to provide a specific ID as well via the column
+ * {@link SearchManager#SUGGEST_COLUMN_INTENT_DATA_ID}, which will be appended to the
+ * intent data URI.
+ *
+ * This can be overriden in any given suggestion using the column
+ * {@link SearchManager#SUGGEST_COLUMN_INTENT_DATA}.
+ *
+ * @return The default intent data, or {@code null} if not set.
+ * @see android.R.styleable#Searchable_searchSuggestIntentData
+ */
+ public String getSuggestIntentData() {
+ return mSuggestIntentData;
+ }
+
+ /**
+ * Gets the suggestion threshold.
+ *
+ * @return The suggestion threshold, or {@code 0} if not set.
+ * @see android.R.styleable#Searchable_searchSuggestThreshold
+ */
+ public int getSuggestThreshold() {
+ return mSuggestThreshold;
+ }
+
+ /**
+ * Get the context for the searchable activity.
+ *
+ * @param context You need to supply a context to start with
+ * @return Returns a context related to the searchable activity
+ * @hide
+ */
+ public Context getActivityContext(Context context) {
+ return createActivityContext(context, mSearchActivity);
+ }
+
+ /**
+ * Creates a context for another activity.
+ */
+ private static Context createActivityContext(Context context, ComponentName activity) {
+ Context theirContext = null;
+ try {
+ theirContext = context.createPackageContext(activity.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(LOG_TAG, "Package not found " + activity.getPackageName());
+ } catch (java.lang.SecurityException e) {
+ Log.e(LOG_TAG, "Can't make context for " + activity.getPackageName(), e);
+ }
+
+ return theirContext;
+ }
+
+ /**
+ * Get the context for the suggestions provider.
+ *
+ * @param context You need to supply a context to start with
+ * @param activityContext If we can determine that the provider and the activity are the
+ * same, we'll just return this one.
+ * @return Returns a context related to the suggestion provider
+ * @hide
+ */
+ public Context getProviderContext(Context context, Context activityContext) {
+ Context theirContext = null;
+ if (mSearchActivity.getPackageName().equals(mSuggestProviderPackage)) {
+ return activityContext;
+ }
+ if (mSuggestProviderPackage != null) {
+ try {
+ theirContext = context.createPackageContext(mSuggestProviderPackage, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ } catch (java.lang.SecurityException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ }
+ }
+ return theirContext;
+ }
+
+ /**
+ * Constructor
+ *
+ * Given a ComponentName, get the searchability info
+ * and build a local copy of it. Use the factory, not this.
+ *
+ * @param activityContext runtime context for the activity that the searchable info is about.
+ * @param attr The attribute set we found in the XML file, contains the values that are used to
+ * construct the object.
+ * @param cName The component name of the searchable activity
+ * @throws IllegalArgumentException if the searchability info is invalid or insufficient
+ */
+ private SearchableInfo(Context activityContext, AttributeSet attr, final ComponentName cName) {
+ mSearchActivity = cName;
+
+ TypedArray a = activityContext.obtainStyledAttributes(attr,
+ com.android.internal.R.styleable.Searchable);
+ mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
+ mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
+ mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0);
+ mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0);
+ mSearchButtonText = a.getResourceId(
+ com.android.internal.R.styleable.Searchable_searchButtonText, 0);
+ mSearchInputType = a.getInt(com.android.internal.R.styleable.Searchable_inputType,
+ InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_NORMAL);
+ mSearchImeOptions = a.getInt(com.android.internal.R.styleable.Searchable_imeOptions,
+ EditorInfo.IME_ACTION_GO);
+ mIncludeInGlobalSearch = a.getBoolean(
+ com.android.internal.R.styleable.Searchable_includeInGlobalSearch, false);
+ mQueryAfterZeroResults = a.getBoolean(
+ com.android.internal.R.styleable.Searchable_queryAfterZeroResults, false);
+ mAutoUrlDetect = a.getBoolean(
+ com.android.internal.R.styleable.Searchable_autoUrlDetect, false);
+
+ mSettingsDescriptionId = a.getResourceId(
+ com.android.internal.R.styleable.Searchable_searchSettingsDescription, 0);
+ mSuggestAuthority = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestAuthority);
+ mSuggestPath = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestPath);
+ mSuggestSelection = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestSelection);
+ mSuggestIntentAction = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentAction);
+ mSuggestIntentData = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentData);
+ mSuggestThreshold = a.getInt(
+ com.android.internal.R.styleable.Searchable_searchSuggestThreshold, 0);
+
+ mVoiceSearchMode =
+ a.getInt(com.android.internal.R.styleable.Searchable_voiceSearchMode, 0);
+ // TODO this didn't work - came back zero from YouTube
+ mVoiceLanguageModeId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguageModel, 0);
+ mVoicePromptTextId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voicePromptText, 0);
+ mVoiceLanguageId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguage, 0);
+ mVoiceMaxResults =
+ a.getInt(com.android.internal.R.styleable.Searchable_voiceMaxResults, 0);
+
+ a.recycle();
+
+ // get package info for suggestions provider (if any)
+ String suggestProviderPackage = null;
+ if (mSuggestAuthority != null) {
+ PackageManager pm = activityContext.getPackageManager();
+ ProviderInfo pi = pm.resolveContentProvider(mSuggestAuthority,
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
+ if (pi != null) {
+ suggestProviderPackage = pi.packageName;
+ }
+ }
+ mSuggestProviderPackage = suggestProviderPackage;
+
+ // for now, implement some form of rules - minimal data
+ if (mLabelId == 0) {
+ throw new IllegalArgumentException("Search label must be a resource reference.");
+ }
+ }
+
+ /**
+ * Information about an action key in searchability meta-data.
+ *
+ * @see SearchableInfo#findActionKey(int)
+ *
+ * @hide This feature is used very little, and on many devices there are no reasonable
+ * keys to use for actions.
+ */
+ public static class ActionKeyInfo implements Parcelable {
+
+ private final int mKeyCode;
+ private final String mQueryActionMsg;
+ private final String mSuggestActionMsg;
+ private final String mSuggestActionMsgColumn;
+
+ /**
+ * Create one object using attributeset as input data.
+ * @param activityContext runtime context of the activity that the action key information
+ * is about.
+ * @param attr The attribute set we found in the XML file, contains the values that are used to
+ * construct the object.
+ * @throws IllegalArgumentException if the action key configuration is invalid
+ */
+ ActionKeyInfo(Context activityContext, AttributeSet attr) {
+ TypedArray a = activityContext.obtainStyledAttributes(attr,
+ com.android.internal.R.styleable.SearchableActionKey);
+
+ mKeyCode = a.getInt(
+ com.android.internal.R.styleable.SearchableActionKey_keycode, 0);
+ mQueryActionMsg = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_queryActionMsg);
+ mSuggestActionMsg = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_suggestActionMsg);
+ mSuggestActionMsgColumn = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_suggestActionMsgColumn);
+ a.recycle();
+
+ // sanity check.
+ if (mKeyCode == 0) {
+ throw new IllegalArgumentException("No keycode.");
+ } else if ((mQueryActionMsg == null) &&
+ (mSuggestActionMsg == null) &&
+ (mSuggestActionMsgColumn == null)) {
+ throw new IllegalArgumentException("No message information.");
+ }
+ }
+
+ /**
+ * Instantiate a new ActionKeyInfo from the data in a Parcel that was
+ * previously written with {@link #writeToParcel(Parcel, int)}.
+ *
+ * @param in The Parcel containing the previously written ActionKeyInfo,
+ * positioned at the location in the buffer where it was written.
+ */
+ private ActionKeyInfo(Parcel in) {
+ mKeyCode = in.readInt();
+ mQueryActionMsg = in.readString();
+ mSuggestActionMsg = in.readString();
+ mSuggestActionMsgColumn = in.readString();
+ }
+
+ /**
+ * Gets the key code that this action key info is for.
+ * @see android.R.styleable#SearchableActionKey_keycode
+ */
+ public int getKeyCode() {
+ return mKeyCode;
+ }
+
+ /**
+ * Gets the action message to use for queries.
+ * @see android.R.styleable#SearchableActionKey_queryActionMsg
+ */
+ public String getQueryActionMsg() {
+ return mQueryActionMsg;
+ }
+
+ /**
+ * Gets the action message to use for suggestions.
+ * @see android.R.styleable#SearchableActionKey_suggestActionMsg
+ */
+ public String getSuggestActionMsg() {
+ return mSuggestActionMsg;
+ }
+
+ /**
+ * Gets the name of the column to get the suggestion action message from.
+ * @see android.R.styleable#SearchableActionKey_suggestActionMsgColumn
+ */
+ public String getSuggestActionMsgColumn() {
+ return mSuggestActionMsgColumn;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mKeyCode);
+ dest.writeString(mQueryActionMsg);
+ dest.writeString(mSuggestActionMsg);
+ dest.writeString(mSuggestActionMsgColumn);
+ }
+ }
+
+ /**
+ * If any action keys were defined for this searchable activity, look up and return.
+ *
+ * @param keyCode The key that was pressed
+ * @return Returns the action key info, or {@code null} if none defined.
+ *
+ * @hide ActionKeyInfo is hidden
+ */
+ public ActionKeyInfo findActionKey(int keyCode) {
+ if (mActionKeys == null) {
+ return null;
+ }
+ return mActionKeys.get(keyCode);
+ }
+
+ private void addActionKey(ActionKeyInfo keyInfo) {
+ if (mActionKeys == null) {
+ mActionKeys = new HashMap<Integer,ActionKeyInfo>();
+ }
+ mActionKeys.put(keyInfo.getKeyCode(), keyInfo);
+ }
+
+ /**
+ * Gets search information for the given activity.
+ *
+ * @param context Context to use for reading activity resources.
+ * @param activityInfo Activity to get search information from.
+ * @return Search information about the given activity, or {@code null} if
+ * the activity has no or invalid searchability meta-data.
+ *
+ * @hide For use by SearchManagerService.
+ */
+ public static SearchableInfo getActivityMetaData(Context context, ActivityInfo activityInfo,
+ int userId) {
+ Context userContext = null;
+ try {
+ userContext = context.createPackageContextAsUser("system", 0,
+ new UserHandle(userId));
+ } catch (NameNotFoundException nnfe) {
+ Log.e(LOG_TAG, "Couldn't create package context for user " + userId);
+ return null;
+ }
+ // for each component, try to find metadata
+ XmlResourceParser xml =
+ activityInfo.loadXmlMetaData(userContext.getPackageManager(), MD_LABEL_SEARCHABLE);
+ if (xml == null) {
+ return null;
+ }
+ ComponentName cName = new ComponentName(activityInfo.packageName, activityInfo.name);
+
+ SearchableInfo searchable = getActivityMetaData(userContext, xml, cName);
+ xml.close();
+
+ if (DBG) {
+ if (searchable != null) {
+ Log.d(LOG_TAG, "Checked " + activityInfo.name
+ + ",label=" + searchable.getLabelId()
+ + ",icon=" + searchable.getIconId()
+ + ",suggestAuthority=" + searchable.getSuggestAuthority()
+ + ",target=" + searchable.getSearchActivity().getClassName()
+ + ",global=" + searchable.shouldIncludeInGlobalSearch()
+ + ",settingsDescription=" + searchable.getSettingsDescriptionId()
+ + ",threshold=" + searchable.getSuggestThreshold());
+ } else {
+ Log.d(LOG_TAG, "Checked " + activityInfo.name + ", no searchable meta-data");
+ }
+ }
+ return searchable;
+ }
+
+ /**
+ * Get the metadata for a given activity
+ *
+ * @param context runtime context
+ * @param xml XML parser for reading attributes
+ * @param cName The component name of the searchable activity
+ *
+ * @result A completely constructed SearchableInfo, or null if insufficient XML data for it
+ */
+ private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
+ final ComponentName cName) {
+ SearchableInfo result = null;
+ Context activityContext = createActivityContext(context, cName);
+ if (activityContext == null) return null;
+
+ // in order to use the attributes mechanism, we have to walk the parser
+ // forward through the file until it's reading the tag of interest.
+ try {
+ int tagType = xml.next();
+ while (tagType != XmlPullParser.END_DOCUMENT) {
+ if (tagType == XmlPullParser.START_TAG) {
+ if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
+ AttributeSet attr = Xml.asAttributeSet(xml);
+ if (attr != null) {
+ try {
+ result = new SearchableInfo(activityContext, attr, cName);
+ } catch (IllegalArgumentException ex) {
+ Log.w(LOG_TAG, "Invalid searchable metadata for " +
+ cName.flattenToShortString() + ": " + ex.getMessage());
+ return null;
+ }
+ }
+ } else if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY)) {
+ if (result == null) {
+ // Can't process an embedded element if we haven't seen the enclosing
+ return null;
+ }
+ AttributeSet attr = Xml.asAttributeSet(xml);
+ if (attr != null) {
+ try {
+ result.addActionKey(new ActionKeyInfo(activityContext, attr));
+ } catch (IllegalArgumentException ex) {
+ Log.w(LOG_TAG, "Invalid action key for " +
+ cName.flattenToShortString() + ": " + ex.getMessage());
+ return null;
+ }
+ }
+ }
+ }
+ tagType = xml.next();
+ }
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Reading searchable metadata for " + cName.flattenToShortString(), e);
+ return null;
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Reading searchable metadata for " + cName.flattenToShortString(), e);
+ return null;
+ }
+
+ return result;
+ }
+
+ /**
+ * Gets the "label" (user-visible name) of this searchable context. This must be
+ * read using the searchable Activity's resources.
+ *
+ * @return A resource id, or {@code 0} if no label was specified.
+ * @see android.R.styleable#Searchable_label
+ *
+ * @hide deprecated functionality
+ */
+ public int getLabelId() {
+ return mLabelId;
+ }
+
+ /**
+ * Gets the resource id of the hint text. This must be
+ * read using the searchable Activity's resources.
+ *
+ * @return A resource id, or {@code 0} if no hint was specified.
+ * @see android.R.styleable#Searchable_hint
+ */
+ public int getHintId() {
+ return mHintId;
+ }
+
+ /**
+ * Gets the icon id specified by the Searchable_icon meta-data entry. This must be
+ * read using the searchable Activity's resources.
+ *
+ * @return A resource id, or {@code 0} if no icon was specified.
+ * @see android.R.styleable#Searchable_icon
+ *
+ * @hide deprecated functionality
+ */
+ public int getIconId() {
+ return mIconId;
+ }
+
+ /**
+ * Checks if the searchable activity wants the voice search button to be shown.
+ *
+ * @see android.R.styleable#Searchable_voiceSearchMode
+ */
+ public boolean getVoiceSearchEnabled() {
+ return 0 != (mVoiceSearchMode & VOICE_SEARCH_SHOW_BUTTON);
+ }
+
+ /**
+ * Checks if voice search should start web search.
+ *
+ * @see android.R.styleable#Searchable_voiceSearchMode
+ */
+ public boolean getVoiceSearchLaunchWebSearch() {
+ return 0 != (mVoiceSearchMode & VOICE_SEARCH_LAUNCH_WEB_SEARCH);
+ }
+
+ /**
+ * Checks if voice search should start in-app search.
+ *
+ * @see android.R.styleable#Searchable_voiceSearchMode
+ */
+ public boolean getVoiceSearchLaunchRecognizer() {
+ return 0 != (mVoiceSearchMode & VOICE_SEARCH_LAUNCH_RECOGNIZER);
+ }
+
+ /**
+ * Gets the resource id of the voice search language model string.
+ *
+ * @return A resource id, or {@code 0} if no language model was specified.
+ * @see android.R.styleable#Searchable_voiceLanguageModel
+ */
+ @StringRes
+ public int getVoiceLanguageModeId() {
+ return mVoiceLanguageModeId;
+ }
+
+ /**
+ * Gets the resource id of the voice prompt text string.
+ *
+ * @return A resource id, or {@code 0} if no voice prompt text was specified.
+ * @see android.R.styleable#Searchable_voicePromptText
+ */
+ @StringRes
+ public int getVoicePromptTextId() {
+ return mVoicePromptTextId;
+ }
+
+ /**
+ * Gets the resource id of the spoken language to recognize in voice search.
+ *
+ * @return A resource id, or {@code 0} if no language was specified.
+ * @see android.R.styleable#Searchable_voiceLanguage
+ */
+ @StringRes
+ public int getVoiceLanguageId() {
+ return mVoiceLanguageId;
+ }
+
+ /**
+ * The maximum number of voice recognition results to return.
+ *
+ * @return the max results count, if specified in the searchable
+ * activity's metadata, or {@code 0} if not specified.
+ * @see android.R.styleable#Searchable_voiceMaxResults
+ */
+ public int getVoiceMaxResults() {
+ return mVoiceMaxResults;
+ }
+
+ /**
+ * Gets the resource id of replacement text for the "Search" button.
+ *
+ * @return A resource id, or {@code 0} if no replacement text was specified.
+ * @see android.R.styleable#Searchable_searchButtonText
+ * @hide This feature is deprecated, no need to add it to the API.
+ */
+ public int getSearchButtonText() {
+ return mSearchButtonText;
+ }
+
+ /**
+ * Gets the input type as specified in the searchable attributes. This will default to
+ * {@link InputType#TYPE_CLASS_TEXT} if not specified (which is appropriate
+ * for free text input).
+ *
+ * @return the input type
+ * @see android.R.styleable#Searchable_inputType
+ */
+ public int getInputType() {
+ return mSearchInputType;
+ }
+
+ /**
+ * Gets the input method options specified in the searchable attributes.
+ * This will default to {@link EditorInfo#IME_ACTION_GO} if not specified (which is
+ * appropriate for a search box).
+ *
+ * @return the input type
+ * @see android.R.styleable#Searchable_imeOptions
+ */
+ public int getImeOptions() {
+ return mSearchImeOptions;
+ }
+
+ /**
+ * Checks whether the searchable should be included in global search.
+ *
+ * @return The value of the {@link android.R.styleable#Searchable_includeInGlobalSearch}
+ * attribute, or {@code false} if the attribute is not set.
+ * @see android.R.styleable#Searchable_includeInGlobalSearch
+ */
+ public boolean shouldIncludeInGlobalSearch() {
+ return mIncludeInGlobalSearch;
+ }
+
+ /**
+ * Checks whether this searchable activity should be queried for suggestions if a prefix
+ * of the query has returned no results.
+ *
+ * @see android.R.styleable#Searchable_queryAfterZeroResults
+ */
+ public boolean queryAfterZeroResults() {
+ return mQueryAfterZeroResults;
+ }
+
+ /**
+ * Checks whether this searchable activity has auto URL detection turned on.
+ *
+ * @see android.R.styleable#Searchable_autoUrlDetect
+ */
+ public boolean autoUrlDetect() {
+ return mAutoUrlDetect;
+ }
+
+ /**
+ * Support for parcelable and aidl operations.
+ */
+ public static final Parcelable.Creator<SearchableInfo> CREATOR
+ = new Parcelable.Creator<SearchableInfo>() {
+ public SearchableInfo createFromParcel(Parcel in) {
+ return new SearchableInfo(in);
+ }
+
+ public SearchableInfo[] newArray(int size) {
+ return new SearchableInfo[size];
+ }
+ };
+
+ /**
+ * Instantiates a new SearchableInfo from the data in a Parcel that was
+ * previously written with {@link #writeToParcel(Parcel, int)}.
+ *
+ * @param in The Parcel containing the previously written SearchableInfo,
+ * positioned at the location in the buffer where it was written.
+ */
+ SearchableInfo(Parcel in) {
+ mLabelId = in.readInt();
+ mSearchActivity = ComponentName.readFromParcel(in);
+ mHintId = in.readInt();
+ mSearchMode = in.readInt();
+ mIconId = in.readInt();
+ mSearchButtonText = in.readInt();
+ mSearchInputType = in.readInt();
+ mSearchImeOptions = in.readInt();
+ mIncludeInGlobalSearch = in.readInt() != 0;
+ mQueryAfterZeroResults = in.readInt() != 0;
+ mAutoUrlDetect = in.readInt() != 0;
+
+ mSettingsDescriptionId = in.readInt();
+ mSuggestAuthority = in.readString();
+ mSuggestPath = in.readString();
+ mSuggestSelection = in.readString();
+ mSuggestIntentAction = in.readString();
+ mSuggestIntentData = in.readString();
+ mSuggestThreshold = in.readInt();
+
+ for (int count = in.readInt(); count > 0; count--) {
+ addActionKey(new ActionKeyInfo(in));
+ }
+
+ mSuggestProviderPackage = in.readString();
+
+ mVoiceSearchMode = in.readInt();
+ mVoiceLanguageModeId = in.readInt();
+ mVoicePromptTextId = in.readInt();
+ mVoiceLanguageId = in.readInt();
+ mVoiceMaxResults = in.readInt();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mLabelId);
+ mSearchActivity.writeToParcel(dest, flags);
+ dest.writeInt(mHintId);
+ dest.writeInt(mSearchMode);
+ dest.writeInt(mIconId);
+ dest.writeInt(mSearchButtonText);
+ dest.writeInt(mSearchInputType);
+ dest.writeInt(mSearchImeOptions);
+ dest.writeInt(mIncludeInGlobalSearch ? 1 : 0);
+ dest.writeInt(mQueryAfterZeroResults ? 1 : 0);
+ dest.writeInt(mAutoUrlDetect ? 1 : 0);
+
+ dest.writeInt(mSettingsDescriptionId);
+ dest.writeString(mSuggestAuthority);
+ dest.writeString(mSuggestPath);
+ dest.writeString(mSuggestSelection);
+ dest.writeString(mSuggestIntentAction);
+ dest.writeString(mSuggestIntentData);
+ dest.writeInt(mSuggestThreshold);
+
+ if (mActionKeys == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(mActionKeys.size());
+ for (ActionKeyInfo actionKey : mActionKeys.values()) {
+ actionKey.writeToParcel(dest, flags);
+ }
+ }
+
+ dest.writeString(mSuggestProviderPackage);
+
+ dest.writeInt(mVoiceSearchMode);
+ dest.writeInt(mVoiceLanguageModeId);
+ dest.writeInt(mVoicePromptTextId);
+ dest.writeInt(mVoiceLanguageId);
+ dest.writeInt(mVoiceMaxResults);
+ }
+}
diff --git a/android/app/Service.java b/android/app/Service.java
new file mode 100644
index 00000000..256c4793
--- /dev/null
+++ b/android/app/Service.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.ComponentCallbacks2;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ContextWrapper;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A Service is an application component representing either an application's desire
+ * to perform a longer-running operation while not interacting with the user
+ * or to supply functionality for other applications to use. Each service
+ * class must have a corresponding
+ * {@link android.R.styleable#AndroidManifestService &lt;service&gt;}
+ * declaration in its package's <code>AndroidManifest.xml</code>. Services
+ * can be started with
+ * {@link android.content.Context#startService Context.startService()} and
+ * {@link android.content.Context#bindService Context.bindService()}.
+ *
+ * <p>Note that services, like other application objects, run in the main
+ * thread of their hosting process. This means that, if your service is going
+ * to do any CPU intensive (such as MP3 playback) or blocking (such as
+ * networking) operations, it should spawn its own thread in which to do that
+ * work. More information on this can be found in
+ * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
+ * Threads</a>. The {@link IntentService} class is available
+ * as a standard implementation of Service that has its own thread where it
+ * schedules its work to be done.</p>
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#WhatIsAService">What is a Service?</a>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
+ * <li><a href="#LocalServiceSample">Local Service Sample</a>
+ * <li><a href="#RemoteMessengerServiceSample">Remote Messenger Service Sample</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a detailed discussion about how to create services, read the
+ * <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="WhatIsAService"></a>
+ * <h3>What is a Service?</h3>
+ *
+ * <p>Most confusion about the Service class actually revolves around what
+ * it is <em>not</em>:</p>
+ *
+ * <ul>
+ * <li> A Service is <b>not</b> a separate process. The Service object itself
+ * does not imply it is running in its own process; unless otherwise specified,
+ * it runs in the same process as the application it is part of.
+ * <li> A Service is <b>not</b> a thread. It is not a means itself to do work off
+ * of the main thread (to avoid Application Not Responding errors).
+ * </ul>
+ *
+ * <p>Thus a Service itself is actually very simple, providing two main features:</p>
+ *
+ * <ul>
+ * <li>A facility for the application to tell the system <em>about</em>
+ * something it wants to be doing in the background (even when the user is not
+ * directly interacting with the application). This corresponds to calls to
+ * {@link android.content.Context#startService Context.startService()}, which
+ * ask the system to schedule work for the service, to be run until the service
+ * or someone else explicitly stop it.
+ * <li>A facility for an application to expose some of its functionality to
+ * other applications. This corresponds to calls to
+ * {@link android.content.Context#bindService Context.bindService()}, which
+ * allows a long-standing connection to be made to the service in order to
+ * interact with it.
+ * </ul>
+ *
+ * <p>When a Service component is actually created, for either of these reasons,
+ * all that the system actually does is instantiate the component
+ * and call its {@link #onCreate} and any other appropriate callbacks on the
+ * main thread. It is up to the Service to implement these with the appropriate
+ * behavior, such as creating a secondary thread in which it does its work.</p>
+ *
+ * <p>Note that because Service itself is so simple, you can make your
+ * interaction with it as simple or complicated as you want: from treating it
+ * as a local Java object that you make direct method calls on (as illustrated
+ * by <a href="#LocalServiceSample">Local Service Sample</a>), to providing
+ * a full remoteable interface using AIDL.</p>
+ *
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ *
+ * <p>There are two reasons that a service can be run by the system. If someone
+ * calls {@link android.content.Context#startService Context.startService()} then the system will
+ * retrieve the service (creating it and calling its {@link #onCreate} method
+ * if needed) and then call its {@link #onStartCommand} method with the
+ * arguments supplied by the client. The service will at this point continue
+ * running until {@link android.content.Context#stopService Context.stopService()} or
+ * {@link #stopSelf()} is called. Note that multiple calls to
+ * Context.startService() do not nest (though they do result in multiple corresponding
+ * calls to onStartCommand()), so no matter how many times it is started a service
+ * will be stopped once Context.stopService() or stopSelf() is called; however,
+ * services can use their {@link #stopSelf(int)} method to ensure the service is
+ * not stopped until started intents have been processed.
+ *
+ * <p>For started services, there are two additional major modes of operation
+ * they can decide to run in, depending on the value they return from
+ * onStartCommand(): {@link #START_STICKY} is used for services that are
+ * explicitly started and stopped as needed, while {@link #START_NOT_STICKY}
+ * or {@link #START_REDELIVER_INTENT} are used for services that should only
+ * remain running while processing any commands sent to them. See the linked
+ * documentation for more detail on the semantics.
+ *
+ * <p>Clients can also use {@link android.content.Context#bindService Context.bindService()} to
+ * obtain a persistent connection to a service. This likewise creates the
+ * service if it is not already running (calling {@link #onCreate} while
+ * doing so), but does not call onStartCommand(). The client will receive the
+ * {@link android.os.IBinder} object that the service returns from its
+ * {@link #onBind} method, allowing the client to then make calls back
+ * to the service. The service will remain running as long as the connection
+ * is established (whether or not the client retains a reference on the
+ * service's IBinder). Usually the IBinder returned is for a complex
+ * interface that has been <a href="{@docRoot}guide/components/aidl.html">written
+ * in aidl</a>.
+ *
+ * <p>A service can be both started and have connections bound to it. In such
+ * a case, the system will keep the service running as long as either it is
+ * started <em>or</em> there are one or more connections to it with the
+ * {@link android.content.Context#BIND_AUTO_CREATE Context.BIND_AUTO_CREATE}
+ * flag. Once neither
+ * of these situations hold, the service's {@link #onDestroy} method is called
+ * and the service is effectively terminated. All cleanup (stopping threads,
+ * unregistering receivers) should be complete upon returning from onDestroy().
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ *
+ * <p>Global access to a service can be enforced when it is declared in its
+ * manifest's {@link android.R.styleable#AndroidManifestService &lt;service&gt;}
+ * tag. By doing so, other applications will need to declare a corresponding
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element in their own manifest to be able to start, stop, or bind to
+ * the service.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#GINGERBREAD}, when using
+ * {@link Context#startService(Intent) Context.startService(Intent)}, you can
+ * also set {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} and/or {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION} on the Intent. This will grant the
+ * Service temporary access to the specific URIs in the Intent. Access will
+ * remain until the Service has called {@link #stopSelf(int)} for that start
+ * command or a later one, or until the Service has been completely stopped.
+ * This works for granting access to the other apps that have not requested
+ * the permission protecting the Service, or even when the Service is not
+ * exported at all.
+ *
+ * <p>In addition, a service can protect individual IPC calls into it with
+ * permissions, by calling the
+ * {@link #checkCallingPermission}
+ * method before executing the implementation of that call.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/security/security.html">Security and Permissions</a>
+ * document for more information on permissions and security in general.
+ *
+ * <a name="ProcessLifecycle"></a>
+ * <h3>Process Lifecycle</h3>
+ *
+ * <p>The Android system will attempt to keep the process hosting a service
+ * around as long as the service has been started or has clients bound to it.
+ * When running low on memory and needing to kill existing processes, the
+ * priority of a process hosting the service will be the higher of the
+ * following possibilities:
+ *
+ * <ul>
+ * <li><p>If the service is currently executing code in its
+ * {@link #onCreate onCreate()}, {@link #onStartCommand onStartCommand()},
+ * or {@link #onDestroy onDestroy()} methods, then the hosting process will
+ * be a foreground process to ensure this code can execute without
+ * being killed.
+ * <li><p>If the service has been started, then its hosting process is considered
+ * to be less important than any processes that are currently visible to the
+ * user on-screen, but more important than any process not visible. Because
+ * only a few processes are generally visible to the user, this means that
+ * the service should not be killed except in low memory conditions. However, since
+ * the user is not directly aware of a background service, in that state it <em>is</em>
+ * considered a valid candidate to kill, and you should be prepared for this to
+ * happen. In particular, long-running services will be increasingly likely to
+ * kill and are guaranteed to be killed (and restarted if appropriate) if they
+ * remain started long enough.
+ * <li><p>If there are clients bound to the service, then the service's hosting
+ * process is never less important than the most important client. That is,
+ * if one of its clients is visible to the user, then the service itself is
+ * considered to be visible. The way a client's importance impacts the service's
+ * importance can be adjusted through {@link Context#BIND_ABOVE_CLIENT},
+ * {@link Context#BIND_ALLOW_OOM_MANAGEMENT}, {@link Context#BIND_WAIVE_PRIORITY},
+ * {@link Context#BIND_IMPORTANT}, and {@link Context#BIND_ADJUST_WITH_ACTIVITY}.
+ * <li><p>A started service can use the {@link #startForeground(int, Notification)}
+ * API to put the service in a foreground state, where the system considers
+ * it to be something the user is actively aware of and thus not a candidate
+ * for killing when low on memory. (It is still theoretically possible for
+ * the service to be killed under extreme memory pressure from the current
+ * foreground application, but in practice this should not be a concern.)
+ * </ul>
+ *
+ * <p>Note this means that most of the time your service is running, it may
+ * be killed by the system if it is under heavy memory pressure. If this
+ * happens, the system will later try to restart the service. An important
+ * consequence of this is that if you implement {@link #onStartCommand onStartCommand()}
+ * to schedule work to be done asynchronously or in another thread, then you
+ * may want to use {@link #START_FLAG_REDELIVERY} to have the system
+ * re-deliver an Intent for you so that it does not get lost if your service
+ * is killed while processing it.
+ *
+ * <p>Other application components running in the same process as the service
+ * (such as an {@link android.app.Activity}) can, of course, increase the
+ * importance of the overall
+ * process beyond just the importance of the service itself.
+ *
+ * <a name="LocalServiceSample"></a>
+ * <h3>Local Service Sample</h3>
+ *
+ * <p>One of the most common uses of a Service is as a secondary component
+ * running alongside other parts of an application, in the same process as
+ * the rest of the components. All components of an .apk run in the same
+ * process unless explicitly stated otherwise, so this is a typical situation.
+ *
+ * <p>When used in this way, by assuming the
+ * components are in the same process, you can greatly simplify the interaction
+ * between them: clients of the service can simply cast the IBinder they
+ * receive from it to a concrete class published by the service.
+ *
+ * <p>An example of this use of a Service is shown here. First is the Service
+ * itself, publishing a custom class when bound:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LocalService.java
+ * service}
+ *
+ * <p>With that done, one can now write client code that directly accesses the
+ * running service, such as:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LocalServiceActivities.java
+ * bind}
+ *
+ * <a name="RemoteMessengerServiceSample"></a>
+ * <h3>Remote Messenger Service Sample</h3>
+ *
+ * <p>If you need to be able to write a Service that can perform complicated
+ * communication with clients in remote processes (beyond simply the use of
+ * {@link Context#startService(Intent) Context.startService} to send
+ * commands to it), then you can use the {@link android.os.Messenger} class
+ * instead of writing full AIDL files.
+ *
+ * <p>An example of a Service that uses Messenger as its client interface
+ * is shown here. First is the Service itself, publishing a Messenger to
+ * an internal Handler when bound:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/MessengerService.java
+ * service}
+ *
+ * <p>If we want to make this service run in a remote process (instead of the
+ * standard one for its .apk), we can use <code>android:process</code> in its
+ * manifest tag to specify one:
+ *
+ * {@sample development/samples/ApiDemos/AndroidManifest.xml remote_service_declaration}
+ *
+ * <p>Note that the name "remote" chosen here is arbitrary, and you can use
+ * other names if you want additional processes. The ':' prefix appends the
+ * name to your package's standard process name.
+ *
+ * <p>With that done, clients can now bind to the service and send messages
+ * to it. Note that this allows clients to register with it to receive
+ * messages back as well:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/MessengerServiceActivities.java
+ * bind}
+ */
+public abstract class Service extends ContextWrapper implements ComponentCallbacks2 {
+ private static final String TAG = "Service";
+
+ /**
+ * Flag for {@link #stopForeground(int)}: if set, the notification previously provided
+ * to {@link #startForeground} will be removed. Otherwise it will remain
+ * until a later call (to {@link #startForeground(int, Notification)} or
+ * {@link #stopForeground(int)} removes it, or the service is destroyed.
+ */
+ public static final int STOP_FOREGROUND_REMOVE = 1<<0;
+
+ /**
+ * Flag for {@link #stopForeground(int)}: if set, the notification previously provided
+ * to {@link #startForeground} will be detached from the service. Only makes sense
+ * when {@link #STOP_FOREGROUND_REMOVE} is <b>not</b> set -- in this case, the notification
+ * will remain shown, but be completely detached from the service and so no longer changed
+ * except through direct calls to the notification manager.
+ */
+ public static final int STOP_FOREGROUND_DETACH = 1<<1;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "STOP_FOREGROUND_" }, value = {
+ STOP_FOREGROUND_REMOVE,
+ STOP_FOREGROUND_DETACH
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StopForegroundFlags {}
+
+ public Service() {
+ super(null);
+ }
+
+ /** Return the application that owns this service. */
+ public final Application getApplication() {
+ return mApplication;
+ }
+
+ /**
+ * Called by the system when the service is first created. Do not call this method directly.
+ */
+ public void onCreate() {
+ }
+
+ /**
+ * @deprecated Implement {@link #onStartCommand(Intent, int, int)} instead.
+ */
+ @Deprecated
+ public void onStart(Intent intent, int startId) {
+ }
+
+ /**
+ * Bits returned by {@link #onStartCommand} describing how to continue
+ * the service if it is killed. May be {@link #START_STICKY},
+ * {@link #START_NOT_STICKY}, {@link #START_REDELIVER_INTENT},
+ * or {@link #START_STICKY_COMPATIBILITY}.
+ */
+ public static final int START_CONTINUATION_MASK = 0xf;
+
+ /**
+ * Constant to return from {@link #onStartCommand}: compatibility
+ * version of {@link #START_STICKY} that does not guarantee that
+ * {@link #onStartCommand} will be called again after being killed.
+ */
+ public static final int START_STICKY_COMPATIBILITY = 0;
+
+ /**
+ * Constant to return from {@link #onStartCommand}: if this service's
+ * process is killed while it is started (after returning from
+ * {@link #onStartCommand}), then leave it in the started state but
+ * don't retain this delivered intent. Later the system will try to
+ * re-create the service. Because it is in the started state, it will
+ * guarantee to call {@link #onStartCommand} after creating the new
+ * service instance; if there are not any pending start commands to be
+ * delivered to the service, it will be called with a null intent
+ * object, so you must take care to check for this.
+ *
+ * <p>This mode makes sense for things that will be explicitly started
+ * and stopped to run for arbitrary periods of time, such as a service
+ * performing background music playback.
+ */
+ public static final int START_STICKY = 1;
+
+ /**
+ * Constant to return from {@link #onStartCommand}: if this service's
+ * process is killed while it is started (after returning from
+ * {@link #onStartCommand}), and there are no new start intents to
+ * deliver to it, then take the service out of the started state and
+ * don't recreate until a future explicit call to
+ * {@link Context#startService Context.startService(Intent)}. The
+ * service will not receive a {@link #onStartCommand(Intent, int, int)}
+ * call with a null Intent because it will not be re-started if there
+ * are no pending Intents to deliver.
+ *
+ * <p>This mode makes sense for things that want to do some work as a
+ * result of being started, but can be stopped when under memory pressure
+ * and will explicit start themselves again later to do more work. An
+ * example of such a service would be one that polls for data from
+ * a server: it could schedule an alarm to poll every N minutes by having
+ * the alarm start its service. When its {@link #onStartCommand} is
+ * called from the alarm, it schedules a new alarm for N minutes later,
+ * and spawns a thread to do its networking. If its process is killed
+ * while doing that check, the service will not be restarted until the
+ * alarm goes off.
+ */
+ public static final int START_NOT_STICKY = 2;
+
+ /**
+ * Constant to return from {@link #onStartCommand}: if this service's
+ * process is killed while it is started (after returning from
+ * {@link #onStartCommand}), then it will be scheduled for a restart
+ * and the last delivered Intent re-delivered to it again via
+ * {@link #onStartCommand}. This Intent will remain scheduled for
+ * redelivery until the service calls {@link #stopSelf(int)} with the
+ * start ID provided to {@link #onStartCommand}. The
+ * service will not receive a {@link #onStartCommand(Intent, int, int)}
+ * call with a null Intent because it will will only be re-started if
+ * it is not finished processing all Intents sent to it (and any such
+ * pending events will be delivered at the point of restart).
+ */
+ public static final int START_REDELIVER_INTENT = 3;
+
+ /** @hide */
+ @IntDef(flag = false, prefix = { "START_" }, value = {
+ START_STICKY_COMPATIBILITY,
+ START_STICKY,
+ START_NOT_STICKY,
+ START_REDELIVER_INTENT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StartResult {}
+
+ /**
+ * Special constant for reporting that we are done processing
+ * {@link #onTaskRemoved(Intent)}.
+ * @hide
+ */
+ public static final int START_TASK_REMOVED_COMPLETE = 1000;
+
+ /**
+ * This flag is set in {@link #onStartCommand} if the Intent is a
+ * re-delivery of a previously delivered intent, because the service
+ * had previously returned {@link #START_REDELIVER_INTENT} but had been
+ * killed before calling {@link #stopSelf(int)} for that Intent.
+ */
+ public static final int START_FLAG_REDELIVERY = 0x0001;
+
+ /**
+ * This flag is set in {@link #onStartCommand} if the Intent is a
+ * retry because the original attempt never got to or returned from
+ * {@link #onStartCommand(Intent, int, int)}.
+ */
+ public static final int START_FLAG_RETRY = 0x0002;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "START_FLAG_" }, value = {
+ START_FLAG_REDELIVERY,
+ START_FLAG_RETRY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StartArgFlags {}
+
+
+ /**
+ * Called by the system every time a client explicitly starts the service by calling
+ * {@link android.content.Context#startService}, providing the arguments it supplied and a
+ * unique integer token representing the start request. Do not call this method directly.
+ *
+ * <p>For backwards compatibility, the default implementation calls
+ * {@link #onStart} and returns either {@link #START_STICKY}
+ * or {@link #START_STICKY_COMPATIBILITY}.
+ *
+ * <p>If you need your application to run on platform versions prior to API
+ * level 5, you can use the following model to handle the older {@link #onStart}
+ * callback in that case. The <code>handleCommand</code> method is implemented by
+ * you as appropriate:
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/ForegroundService.java
+ * start_compatibility}
+ *
+ * <p class="caution">Note that the system calls this on your
+ * service's main thread. A service's main thread is the same
+ * thread where UI operations take place for Activities running in the
+ * same process. You should always avoid stalling the main
+ * thread's event loop. When doing long-running operations,
+ * network calls, or heavy disk I/O, you should kick off a new
+ * thread, or use {@link android.os.AsyncTask}.</p>
+ *
+ * @param intent The Intent supplied to {@link android.content.Context#startService},
+ * as given. This may be null if the service is being restarted after
+ * its process has gone away, and it had previously returned anything
+ * except {@link #START_STICKY_COMPATIBILITY}.
+ * @param flags Additional data about this start request.
+ * @param startId A unique integer representing this specific request to
+ * start. Use with {@link #stopSelfResult(int)}.
+ *
+ * @return The return value indicates what semantics the system should
+ * use for the service's current started state. It may be one of the
+ * constants associated with the {@link #START_CONTINUATION_MASK} bits.
+ *
+ * @see #stopSelfResult(int)
+ */
+ public @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
+ onStart(intent, startId);
+ return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY;
+ }
+
+ /**
+ * Called by the system to notify a Service that it is no longer used and is being removed. The
+ * service should clean up any resources it holds (threads, registered
+ * receivers, etc) at this point. Upon return, there will be no more calls
+ * in to this Service object and it is effectively dead. Do not call this method directly.
+ */
+ public void onDestroy() {
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ public void onLowMemory() {
+ }
+
+ public void onTrimMemory(int level) {
+ }
+
+ /**
+ * Return the communication channel to the service. May return null if
+ * clients can not bind to the service. The returned
+ * {@link android.os.IBinder} is usually for a complex interface
+ * that has been <a href="{@docRoot}guide/components/aidl.html">described using
+ * aidl</a>.
+ *
+ * <p><em>Note that unlike other application components, calls on to the
+ * IBinder interface returned here may not happen on the main thread
+ * of the process</em>. More information about the main thread can be found in
+ * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
+ * Threads</a>.</p>
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ *
+ * @return Return an IBinder through which clients can call on to the
+ * service.
+ */
+ @Nullable
+ public abstract IBinder onBind(Intent intent);
+
+ /**
+ * Called when all clients have disconnected from a particular interface
+ * published by the service. The default implementation does nothing and
+ * returns false.
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ *
+ * @return Return true if you would like to have the service's
+ * {@link #onRebind} method later called when new clients bind to it.
+ */
+ public boolean onUnbind(Intent intent) {
+ return false;
+ }
+
+ /**
+ * Called when new clients have connected to the service, after it had
+ * previously been notified that all had disconnected in its
+ * {@link #onUnbind}. This will only be called if the implementation
+ * of {@link #onUnbind} was overridden to return true.
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ */
+ public void onRebind(Intent intent) {
+ }
+
+ /**
+ * This is called if the service is currently running and the user has
+ * removed a task that comes from the service's application. If you have
+ * set {@link android.content.pm.ServiceInfo#FLAG_STOP_WITH_TASK ServiceInfo.FLAG_STOP_WITH_TASK}
+ * then you will not receive this callback; instead, the service will simply
+ * be stopped.
+ *
+ * @param rootIntent The original root Intent that was used to launch
+ * the task that is being removed.
+ */
+ public void onTaskRemoved(Intent rootIntent) {
+ }
+
+ /**
+ * Stop the service, if it was previously started. This is the same as
+ * calling {@link android.content.Context#stopService} for this particular service.
+ *
+ * @see #stopSelfResult(int)
+ */
+ public final void stopSelf() {
+ stopSelf(-1);
+ }
+
+ /**
+ * Old version of {@link #stopSelfResult} that doesn't return a result.
+ *
+ * @see #stopSelfResult
+ */
+ public final void stopSelf(int startId) {
+ if (mActivityManager == null) {
+ return;
+ }
+ try {
+ mActivityManager.stopServiceToken(
+ new ComponentName(this, mClassName), mToken, startId);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Stop the service if the most recent time it was started was
+ * <var>startId</var>. This is the same as calling {@link
+ * android.content.Context#stopService} for this particular service but allows you to
+ * safely avoid stopping if there is a start request from a client that you
+ * haven't yet seen in {@link #onStart}.
+ *
+ * <p><em>Be careful about ordering of your calls to this function.</em>.
+ * If you call this function with the most-recently received ID before
+ * you have called it for previously received IDs, the service will be
+ * immediately stopped anyway. If you may end up processing IDs out
+ * of order (such as by dispatching them on separate threads), then you
+ * are responsible for stopping them in the same order you received them.</p>
+ *
+ * @param startId The most recent start identifier received in {@link
+ * #onStart}.
+ * @return Returns true if the startId matches the last start request
+ * and the service will be stopped, else false.
+ *
+ * @see #stopSelf()
+ */
+ public final boolean stopSelfResult(int startId) {
+ if (mActivityManager == null) {
+ return false;
+ }
+ try {
+ return mActivityManager.stopServiceToken(
+ new ComponentName(this, mClassName), mToken, startId);
+ } catch (RemoteException ex) {
+ }
+ return false;
+ }
+
+ /**
+ * @deprecated This is a now a no-op, use
+ * {@link #startForeground(int, Notification)} instead. This method
+ * has been turned into a no-op rather than simply being deprecated
+ * because analysis of numerous poorly behaving devices has shown that
+ * increasingly often the trouble is being caused in part by applications
+ * that are abusing it. Thus, given a choice between introducing
+ * problems in existing applications using this API (by allowing them to
+ * be killed when they would like to avoid it), vs allowing the performance
+ * of the entire system to be decreased, this method was deemed less
+ * important.
+ *
+ * @hide
+ */
+ @Deprecated
+ public final void setForeground(boolean isForeground) {
+ Log.w(TAG, "setForeground: ignoring old API call on " + getClass().getName());
+ }
+
+ /**
+ * If your service is started (running through {@link Context#startService(Intent)}), then
+ * also make this service run in the foreground, supplying the ongoing
+ * notification to be shown to the user while in this state.
+ * By default started services are background, meaning that their process won't be given
+ * foreground CPU scheduling (unless something else in that process is foreground) and,
+ * if the system needs to kill them to reclaim more memory (such as to display a large page in a
+ * web browser), they can be killed without too much harm. You use
+ * {@link #startForeground} if killing your service would be disruptive to the user, such as
+ * if your service is performing background music playback, so the user
+ * would notice if their music stopped playing.
+ *
+ * <p>Note that calling this method does <em>not</em> put the service in the started state
+ * itself, even though the name sounds like it. You must always call
+ * {@link #startService(Intent)} first to tell the system it should keep the service running,
+ * and then use this method to tell it to keep it running harder.</p>
+ *
+ * @param id The identifier for this notification as per
+ * {@link NotificationManager#notify(int, Notification)
+ * NotificationManager.notify(int, Notification)}; must not be 0.
+ * @param notification The Notification to be displayed.
+ *
+ * @see #stopForeground(boolean)
+ */
+ public final void startForeground(int id, Notification notification) {
+ try {
+ mActivityManager.setServiceForeground(
+ new ComponentName(this, mClassName), mToken, id,
+ notification, 0);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Synonym for {@link #stopForeground(int)}.
+ * @param removeNotification If true, the {@link #STOP_FOREGROUND_REMOVE} flag
+ * will be supplied.
+ * @see #stopForeground(int)
+ * @see #startForeground(int, Notification)
+ */
+ public final void stopForeground(boolean removeNotification) {
+ stopForeground(removeNotification ? STOP_FOREGROUND_REMOVE : 0);
+ }
+
+ /**
+ * Remove this service from foreground state, allowing it to be killed if
+ * more memory is needed. This does not stop the service from running (for that
+ * you use {@link #stopSelf()} or related methods), just takes it out of the
+ * foreground state.
+ *
+ * @param flags additional behavior options.
+ * @see #startForeground(int, Notification)
+ */
+ public final void stopForeground(@StopForegroundFlags int flags) {
+ try {
+ mActivityManager.setServiceForeground(
+ new ComponentName(this, mClassName), mToken, 0, null, flags);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Print the Service's state into the given stream. This gets invoked if
+ * you run "adb shell dumpsys activity service &lt;yourservicename&gt;"
+ * (note that for this command to work, the service must be running, and
+ * you must specify a fully-qualified service name).
+ * This is distinct from "dumpsys &lt;servicename&gt;", which only works for
+ * named system services and which invokes the {@link IBinder#dump} method
+ * on the {@link IBinder} interface registered with ServiceManager.
+ *
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("nothing to dump");
+ }
+
+ // ------------------ Internal API ------------------
+
+ /**
+ * @hide
+ */
+ public final void attach(
+ Context context,
+ ActivityThread thread, String className, IBinder token,
+ Application application, Object activityManager) {
+ attachBaseContext(context);
+ mThread = thread; // NOTE: unused - remove?
+ mClassName = className;
+ mToken = token;
+ mApplication = application;
+ mActivityManager = (IActivityManager)activityManager;
+ mStartCompatibility = getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.ECLAIR;
+ }
+
+ /**
+ * @hide
+ * Clean up any references to avoid leaks.
+ */
+ public final void detachAndCleanUp() {
+ mToken = null;
+ }
+
+ final String getClassName() {
+ return mClassName;
+ }
+
+ // set by the thread after the constructor and before onCreate(Bundle icicle) is called.
+ private ActivityThread mThread = null;
+ private String mClassName = null;
+ private IBinder mToken = null;
+ private Application mApplication = null;
+ private IActivityManager mActivityManager = null;
+ private boolean mStartCompatibility = false;
+}
diff --git a/android/app/ServiceStartArgs.java b/android/app/ServiceStartArgs.java
new file mode 100644
index 00000000..f030cbaa
--- /dev/null
+++ b/android/app/ServiceStartArgs.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Describes a Service.onStartCommand() request from the system.
+ * @hide
+ */
+public class ServiceStartArgs implements Parcelable {
+ final public boolean taskRemoved;
+ final public int startId;
+ final public int flags;
+ final public Intent args;
+
+ public ServiceStartArgs(boolean _taskRemoved, int _startId, int _flags, Intent _args) {
+ taskRemoved = _taskRemoved;
+ startId = _startId;
+ flags = _flags;
+ args = _args;
+ }
+
+ public String toString() {
+ return "ServiceStartArgs{taskRemoved=" + taskRemoved + ", startId=" + startId
+ + ", flags=0x" + Integer.toHexString(flags) + ", args=" + args + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(taskRemoved ? 1 : 0);
+ out.writeInt(startId);
+ out.writeInt(flags);
+ if (args != null) {
+ out.writeInt(1);
+ args.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Parcelable.Creator<ServiceStartArgs> CREATOR
+ = new Parcelable.Creator<ServiceStartArgs>() {
+ public ServiceStartArgs createFromParcel(Parcel in) {
+ return new ServiceStartArgs(in);
+ }
+
+ public ServiceStartArgs[] newArray(int size) {
+ return new ServiceStartArgs[size];
+ }
+ };
+
+ public ServiceStartArgs(Parcel in) {
+ taskRemoved = in.readInt() != 0;
+ startId = in.readInt();
+ flags = in.readInt();
+ if (in.readInt() != 0) {
+ args = Intent.CREATOR.createFromParcel(in);
+ } else {
+ args = null;
+ }
+ }
+}
diff --git a/android/app/SharedElementCallback.java b/android/app/SharedElementCallback.java
new file mode 100644
index 00000000..af13e695
--- /dev/null
+++ b/android/app/SharedElementCallback.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.GraphicBuffer;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.transition.TransitionUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Listener provided in
+ * {@link Activity#setEnterSharedElementCallback(SharedElementCallback)} and
+ * {@link Activity#setExitSharedElementCallback(SharedElementCallback)} as well as
+ * {@link Fragment#setEnterSharedElementCallback(SharedElementCallback)} and
+ * {@link Fragment#setExitSharedElementCallback(SharedElementCallback)}
+ * to monitor the Shared element transitions. The events can be used to customize Activity
+ * and Fragment Transition behavior.
+ */
+public abstract class SharedElementCallback {
+ private Matrix mTempMatrix;
+ private static final String BUNDLE_SNAPSHOT_BITMAP = "sharedElement:snapshot:bitmap";
+ private static final String BUNDLE_SNAPSHOT_GRAPHIC_BUFFER =
+ "sharedElement:snapshot:graphicBuffer";
+ private static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "sharedElement:snapshot:imageScaleType";
+ private static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "sharedElement:snapshot:imageMatrix";
+
+ static final SharedElementCallback NULL_CALLBACK = new SharedElementCallback() {
+ };
+
+ /**
+ * In Activity Transitions, onSharedElementStart is called immediately before
+ * capturing the start of the shared element state on enter and reenter transitions and
+ * immediately before capturing the end of the shared element state for exit and return
+ * transitions.
+ * <p>
+ * In Fragment Transitions, onSharedElementStart is called immediately before capturing the
+ * start state of all shared element transitions.
+ * <p>
+ * This call can be used to adjust the transition start state by modifying the shared
+ * element Views. Note that no layout step will be executed between onSharedElementStart
+ * and the transition state capture.
+ * <p>
+ * For Activity Transitions, any changes made in {@link #onSharedElementEnd(List, List, List)}
+ * that are not updated during by layout should be corrected in onSharedElementStart for exit and
+ * return transitions. For example, rotation or scale will not be affected by layout and
+ * if changed in {@link #onSharedElementEnd(List, List, List)}, it will also have to be reset
+ * in onSharedElementStart again to correct the end state.
+ *
+ * @param sharedElementNames The names of the shared elements that were accepted into
+ * the View hierarchy.
+ * @param sharedElements The shared elements that are part of the View hierarchy.
+ * @param sharedElementSnapshots The Views containing snap shots of the shared element
+ * from the launching Window. These elements will not
+ * be part of the scene, but will be positioned relative
+ * to the Window decor View. This list is null for Fragment
+ * Transitions.
+ */
+ public void onSharedElementStart(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {}
+
+ /**
+ * In Activity Transitions, onSharedElementEnd is called immediately before
+ * capturing the end of the shared element state on enter and reenter transitions and
+ * immediately before capturing the start of the shared element state for exit and return
+ * transitions.
+ * <p>
+ * In Fragment Transitions, onSharedElementEnd is called immediately before capturing the
+ * end state of all shared element transitions.
+ * <p>
+ * This call can be used to adjust the transition end state by modifying the shared
+ * element Views. Note that no layout step will be executed between onSharedElementEnd
+ * and the transition state capture.
+ * <p>
+ * Any changes made in {@link #onSharedElementStart(List, List, List)} that are not updated
+ * during layout should be corrected in onSharedElementEnd. For example, rotation or scale
+ * will not be affected by layout and if changed in
+ * {@link #onSharedElementStart(List, List, List)}, it will also have to be reset in
+ * onSharedElementEnd again to correct the end state.
+ *
+ * @param sharedElementNames The names of the shared elements that were accepted into
+ * the View hierarchy.
+ * @param sharedElements The shared elements that are part of the View hierarchy.
+ * @param sharedElementSnapshots The Views containing snap shots of the shared element
+ * from the launching Window. These elements will not
+ * be part of the scene, but will be positioned relative
+ * to the Window decor View. This list will be null for
+ * Fragment Transitions.
+ */
+ public void onSharedElementEnd(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {}
+
+ /**
+ * Called after {@link #onMapSharedElements(java.util.List, java.util.Map)} when
+ * transferring shared elements in. Any shared elements that have no mapping will be in
+ * <var>rejectedSharedElements</var>. The elements remaining in
+ * <var>rejectedSharedElements</var> will be transitioned out of the Scene. If a
+ * View is removed from <var>rejectedSharedElements</var>, it must be handled by the
+ * <code>SharedElementCallback</code>.
+ * <p>
+ * Views in rejectedSharedElements will have their position and size set to the
+ * position of the calling shared element, relative to the Window decor View and contain
+ * snapshots of the View from the calling Activity or Fragment. This
+ * view may be safely added to the decor View's overlay to remain in position.
+ * </p>
+ * <p>This method is not called for Fragment Transitions. All rejected shared elements
+ * will be handled by the exit transition.</p>
+ *
+ * @param rejectedSharedElements Views containing visual information of shared elements
+ * that are not part of the entering scene. These Views
+ * are positioned relative to the Window decor View. A
+ * View removed from this list will not be transitioned
+ * automatically.
+ */
+ public void onRejectSharedElements(List<View> rejectedSharedElements) {}
+
+ /**
+ * Lets the SharedElementCallback adjust the mapping of shared element names to
+ * Views.
+ *
+ * @param names The names of all shared elements transferred from the calling Activity
+ * or Fragment in the order they were provided.
+ * @param sharedElements The mapping of shared element names to Views. The best guess
+ * will be filled into sharedElements based on the transitionNames.
+ */
+ public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {}
+
+ /**
+ * Creates a snapshot of a shared element to be used by the remote Activity and reconstituted
+ * with {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)}. A
+ * null return value will mean that the remote Activity will have a null snapshot View in
+ * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and
+ * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}.
+ *
+ * <p>This is not called for Fragment Transitions.</p>
+ *
+ * @param sharedElement The shared element View to create a snapshot for.
+ * @param viewToGlobalMatrix A matrix containing a transform from the view to the screen
+ * coordinates.
+ * @param screenBounds The bounds of shared element in screen coordinate space. This is
+ * the bounds of the view with the viewToGlobalMatrix applied.
+ * @return A snapshot to send to the remote Activity to be reconstituted with
+ * {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)} and passed
+ * into {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and
+ * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}.
+ */
+ public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix,
+ RectF screenBounds) {
+ if (sharedElement instanceof ImageView) {
+ ImageView imageView = ((ImageView) sharedElement);
+ Drawable d = imageView.getDrawable();
+ Drawable bg = imageView.getBackground();
+ if (d != null && (bg == null || bg.getAlpha() == 0)) {
+ Bitmap bitmap = TransitionUtils.createDrawableBitmap(d);
+ if (bitmap != null) {
+ Bundle bundle = new Bundle();
+ if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
+ bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap);
+ } else {
+ GraphicBuffer graphicBuffer = bitmap.createGraphicBufferHandle();
+ bundle.putParcelable(BUNDLE_SNAPSHOT_GRAPHIC_BUFFER, graphicBuffer);
+ }
+ bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE,
+ imageView.getScaleType().toString());
+ if (imageView.getScaleType() == ScaleType.MATRIX) {
+ Matrix matrix = imageView.getImageMatrix();
+ float[] values = new float[9];
+ matrix.getValues(values);
+ bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values);
+ }
+ return bundle;
+ }
+ }
+ }
+ if (mTempMatrix == null) {
+ mTempMatrix = new Matrix(viewToGlobalMatrix);
+ } else {
+ mTempMatrix.set(viewToGlobalMatrix);
+ }
+ return TransitionUtils.createViewBitmap(sharedElement, mTempMatrix, screenBounds);
+ }
+
+ /**
+ * Reconstitutes a snapshot View from a Parcelable returned in
+ * {@link #onCaptureSharedElementSnapshot(android.view.View, android.graphics.Matrix,
+ * android.graphics.RectF)} to be used in {@link #onSharedElementStart(java.util.List,
+ * java.util.List, java.util.List)} and {@link #onSharedElementEnd(java.util.List,
+ * java.util.List, java.util.List)}. The returned View will be sized and positioned after
+ * this call so that it is ready to be added to the decor View's overlay.
+ *
+ * <p>This is not called for Fragment Transitions.</p>
+ *
+ * @param context The Context used to create the snapshot View.
+ * @param snapshot The Parcelable returned by {@link #onCaptureSharedElementSnapshot(
+ * android.view.View, android.graphics.Matrix, android.graphics.RectF)}.
+ * @return A View to be sent in {@link #onSharedElementStart(java.util.List, java.util.List,
+ * java.util.List)} and {@link #onSharedElementEnd(java.util.List, java.util.List,
+ * java.util.List)}. A null value will produce a null snapshot value for those two methods.
+ */
+ public View onCreateSnapshotView(Context context, Parcelable snapshot) {
+ View view = null;
+ if (snapshot instanceof Bundle) {
+ Bundle bundle = (Bundle) snapshot;
+ GraphicBuffer buffer = bundle.getParcelable(BUNDLE_SNAPSHOT_GRAPHIC_BUFFER);
+ Bitmap bitmap = bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP);
+ if (buffer == null && bitmap == null) {
+ return null;
+ }
+ if (bitmap == null) {
+ bitmap = Bitmap.createHardwareBitmap(buffer);
+ }
+ ImageView imageView = new ImageView(context);
+ view = imageView;
+ imageView.setImageBitmap(bitmap);
+ imageView.setScaleType(
+ ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE)));
+ if (imageView.getScaleType() == ScaleType.MATRIX) {
+ float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX);
+ Matrix matrix = new Matrix();
+ matrix.setValues(values);
+ imageView.setImageMatrix(matrix);
+ }
+ } else if (snapshot instanceof Bitmap) {
+ Bitmap bitmap = (Bitmap) snapshot;
+ view = new View(context);
+ Resources resources = context.getResources();
+ view.setBackground(new BitmapDrawable(resources, bitmap));
+ }
+ return view;
+ }
+
+ /**
+ * Called during an Activity Transition when the shared elements have arrived at the
+ * final location and are ready to be transferred. This method is called for both the
+ * source and destination Activities.
+ * <p>
+ * When the shared elements are ready to be transferred,
+ * {@link OnSharedElementsReadyListener#onSharedElementsReady()}
+ * must be called to trigger the transfer.
+ * <p>
+ * The default behavior is to trigger the transfer immediately.
+ *
+ * @param sharedElementNames The names of the shared elements that are being transferred..
+ * @param sharedElements The shared elements that are part of the View hierarchy.
+ * @param listener The listener to call when the shared elements are ready to be hidden
+ * in the source Activity or shown in the destination Activity.
+ */
+ public void onSharedElementsArrived(List<String> sharedElementNames,
+ List<View> sharedElements, OnSharedElementsReadyListener listener) {
+ listener.onSharedElementsReady();
+ }
+
+ /**
+ * Listener to be called after {@link
+ * SharedElementCallback#onSharedElementsArrived(List, List, OnSharedElementsReadyListener)}
+ * when the shared elements are ready to be hidden in the source Activity and shown in the
+ * destination Activity.
+ */
+ public interface OnSharedElementsReadyListener {
+
+ /**
+ * Call this method during or after the OnSharedElementsReadyListener has been received
+ * in {@link SharedElementCallback#onSharedElementsArrived(List, List,
+ * OnSharedElementsReadyListener)} to indicate that the shared elements are ready to be
+ * hidden in the source and shown in the destination Activity.
+ */
+ void onSharedElementsReady();
+ }
+}
diff --git a/android/app/SharedPreferencesImpl.java b/android/app/SharedPreferencesImpl.java
new file mode 100644
index 00000000..6ea08252
--- /dev/null
+++ b/android/app/SharedPreferencesImpl.java
@@ -0,0 +1,804 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.Nullable;
+import android.content.SharedPreferences;
+import android.os.FileUtils;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.system.StructTimespec;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ExponentiallyBucketedHistogram;
+import com.android.internal.util.XmlUtils;
+
+import dalvik.system.BlockGuard;
+
+import libcore.io.IoUtils;
+
+import com.google.android.collect.Maps;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CountDownLatch;
+
+final class SharedPreferencesImpl implements SharedPreferences {
+ private static final String TAG = "SharedPreferencesImpl";
+ private static final boolean DEBUG = false;
+ private static final Object CONTENT = new Object();
+
+ /** If a fsync takes more than {@value #MAX_FSYNC_DURATION_MILLIS} ms, warn */
+ private static final long MAX_FSYNC_DURATION_MILLIS = 256;
+
+ // Lock ordering rules:
+ // - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
+ // - acquire mWritingToDiskLock before EditorImpl.mLock
+
+ private final File mFile;
+ private final File mBackupFile;
+ private final int mMode;
+ private final Object mLock = new Object();
+ private final Object mWritingToDiskLock = new Object();
+
+ @GuardedBy("mLock")
+ private Map<String, Object> mMap;
+
+ @GuardedBy("mLock")
+ private int mDiskWritesInFlight = 0;
+
+ @GuardedBy("mLock")
+ private boolean mLoaded = false;
+
+ @GuardedBy("mLock")
+ private StructTimespec mStatTimestamp;
+
+ @GuardedBy("mLock")
+ private long mStatSize;
+
+ @GuardedBy("mLock")
+ private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
+ new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
+
+ /** Current memory state (always increasing) */
+ @GuardedBy("this")
+ private long mCurrentMemoryStateGeneration;
+
+ /** Latest memory state that was committed to disk */
+ @GuardedBy("mWritingToDiskLock")
+ private long mDiskStateGeneration;
+
+ /** Time (and number of instances) of file-system sync requests */
+ @GuardedBy("mWritingToDiskLock")
+ private final ExponentiallyBucketedHistogram mSyncTimes = new ExponentiallyBucketedHistogram(16);
+ private int mNumSync = 0;
+
+ SharedPreferencesImpl(File file, int mode) {
+ mFile = file;
+ mBackupFile = makeBackupFile(file);
+ mMode = mode;
+ mLoaded = false;
+ mMap = null;
+ startLoadFromDisk();
+ }
+
+ private void startLoadFromDisk() {
+ synchronized (mLock) {
+ mLoaded = false;
+ }
+ new Thread("SharedPreferencesImpl-load") {
+ public void run() {
+ loadFromDisk();
+ }
+ }.start();
+ }
+
+ private void loadFromDisk() {
+ synchronized (mLock) {
+ if (mLoaded) {
+ return;
+ }
+ if (mBackupFile.exists()) {
+ mFile.delete();
+ mBackupFile.renameTo(mFile);
+ }
+ }
+
+ // Debugging
+ if (mFile.exists() && !mFile.canRead()) {
+ Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
+ }
+
+ Map map = null;
+ StructStat stat = null;
+ try {
+ stat = Os.stat(mFile.getPath());
+ if (mFile.canRead()) {
+ BufferedInputStream str = null;
+ try {
+ str = new BufferedInputStream(
+ new FileInputStream(mFile), 16*1024);
+ map = XmlUtils.readMapXml(str);
+ } catch (Exception e) {
+ Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
+ } finally {
+ IoUtils.closeQuietly(str);
+ }
+ }
+ } catch (ErrnoException e) {
+ /* ignore */
+ }
+
+ synchronized (mLock) {
+ mLoaded = true;
+ if (map != null) {
+ mMap = map;
+ mStatTimestamp = stat.st_mtim;
+ mStatSize = stat.st_size;
+ } else {
+ mMap = new HashMap<>();
+ }
+ mLock.notifyAll();
+ }
+ }
+
+ static File makeBackupFile(File prefsFile) {
+ return new File(prefsFile.getPath() + ".bak");
+ }
+
+ void startReloadIfChangedUnexpectedly() {
+ synchronized (mLock) {
+ // TODO: wait for any pending writes to disk?
+ if (!hasFileChangedUnexpectedly()) {
+ return;
+ }
+ startLoadFromDisk();
+ }
+ }
+
+ // Has the file changed out from under us? i.e. writes that
+ // we didn't instigate.
+ private boolean hasFileChangedUnexpectedly() {
+ synchronized (mLock) {
+ if (mDiskWritesInFlight > 0) {
+ // If we know we caused it, it's not unexpected.
+ if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
+ return false;
+ }
+ }
+
+ final StructStat stat;
+ try {
+ /*
+ * Metadata operations don't usually count as a block guard
+ * violation, but we explicitly want this one.
+ */
+ BlockGuard.getThreadPolicy().onReadFromDisk();
+ stat = Os.stat(mFile.getPath());
+ } catch (ErrnoException e) {
+ return true;
+ }
+
+ synchronized (mLock) {
+ return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
+ }
+ }
+
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(mLock) {
+ mListeners.put(listener, CONTENT);
+ }
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(mLock) {
+ mListeners.remove(listener);
+ }
+ }
+
+ private void awaitLoadedLocked() {
+ if (!mLoaded) {
+ // Raise an explicit StrictMode onReadFromDisk for this
+ // thread, since the real read will be in a different
+ // thread and otherwise ignored by StrictMode.
+ BlockGuard.getThreadPolicy().onReadFromDisk();
+ }
+ while (!mLoaded) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException unused) {
+ }
+ }
+ }
+
+ public Map<String, ?> getAll() {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ //noinspection unchecked
+ return new HashMap<String, Object>(mMap);
+ }
+ }
+
+ @Nullable
+ public String getString(String key, @Nullable String defValue) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ String v = (String)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ @Nullable
+ public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ Set<String> v = (Set<String>) mMap.get(key);
+ return v != null ? v : defValues;
+ }
+ }
+
+ public int getInt(String key, int defValue) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ Integer v = (Integer)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public long getLong(String key, long defValue) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ Long v = (Long)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public float getFloat(String key, float defValue) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ Float v = (Float)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public boolean getBoolean(String key, boolean defValue) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ Boolean v = (Boolean)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public boolean contains(String key) {
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ return mMap.containsKey(key);
+ }
+ }
+
+ public Editor edit() {
+ // TODO: remove the need to call awaitLoadedLocked() when
+ // requesting an editor. will require some work on the
+ // Editor, but then we should be able to do:
+ //
+ // context.getSharedPreferences(..).edit().putString(..).apply()
+ //
+ // ... all without blocking.
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ }
+
+ return new EditorImpl();
+ }
+
+ // Return value from EditorImpl#commitToMemory()
+ private static class MemoryCommitResult {
+ final long memoryStateGeneration;
+ @Nullable final List<String> keysModified;
+ @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
+ final Map<String, Object> mapToWriteToDisk;
+ final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
+
+ @GuardedBy("mWritingToDiskLock")
+ volatile boolean writeToDiskResult = false;
+ boolean wasWritten = false;
+
+ private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
+ @Nullable Set<OnSharedPreferenceChangeListener> listeners,
+ Map<String, Object> mapToWriteToDisk) {
+ this.memoryStateGeneration = memoryStateGeneration;
+ this.keysModified = keysModified;
+ this.listeners = listeners;
+ this.mapToWriteToDisk = mapToWriteToDisk;
+ }
+
+ void setDiskWriteResult(boolean wasWritten, boolean result) {
+ this.wasWritten = wasWritten;
+ writeToDiskResult = result;
+ writtenToDiskLatch.countDown();
+ }
+ }
+
+ public final class EditorImpl implements Editor {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final Map<String, Object> mModified = Maps.newHashMap();
+
+ @GuardedBy("mLock")
+ private boolean mClear = false;
+
+ public Editor putString(String key, @Nullable String value) {
+ synchronized (mLock) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putStringSet(String key, @Nullable Set<String> values) {
+ synchronized (mLock) {
+ mModified.put(key,
+ (values == null) ? null : new HashSet<String>(values));
+ return this;
+ }
+ }
+ public Editor putInt(String key, int value) {
+ synchronized (mLock) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putLong(String key, long value) {
+ synchronized (mLock) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putFloat(String key, float value) {
+ synchronized (mLock) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putBoolean(String key, boolean value) {
+ synchronized (mLock) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor remove(String key) {
+ synchronized (mLock) {
+ mModified.put(key, this);
+ return this;
+ }
+ }
+
+ public Editor clear() {
+ synchronized (mLock) {
+ mClear = true;
+ return this;
+ }
+ }
+
+ public void apply() {
+ final long startTime = System.currentTimeMillis();
+
+ final MemoryCommitResult mcr = commitToMemory();
+ final Runnable awaitCommit = new Runnable() {
+ public void run() {
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException ignored) {
+ }
+
+ if (DEBUG && mcr.wasWritten) {
+ Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ + " applied after " + (System.currentTimeMillis() - startTime)
+ + " ms");
+ }
+ }
+ };
+
+ QueuedWork.addFinisher(awaitCommit);
+
+ Runnable postWriteRunnable = new Runnable() {
+ public void run() {
+ awaitCommit.run();
+ QueuedWork.removeFinisher(awaitCommit);
+ }
+ };
+
+ SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
+
+ // Okay to notify the listeners before it's hit disk
+ // because the listeners should always get the same
+ // SharedPreferences instance back, which has the
+ // changes reflected in memory.
+ notifyListeners(mcr);
+ }
+
+ // Returns true if any changes were made
+ private MemoryCommitResult commitToMemory() {
+ long memoryStateGeneration;
+ List<String> keysModified = null;
+ Set<OnSharedPreferenceChangeListener> listeners = null;
+ Map<String, Object> mapToWriteToDisk;
+
+ synchronized (SharedPreferencesImpl.this.mLock) {
+ // We optimistically don't make a deep copy until
+ // a memory commit comes in when we're already
+ // writing to disk.
+ if (mDiskWritesInFlight > 0) {
+ // We can't modify our mMap as a currently
+ // in-flight write owns it. Clone it before
+ // modifying it.
+ // noinspection unchecked
+ mMap = new HashMap<String, Object>(mMap);
+ }
+ mapToWriteToDisk = mMap;
+ mDiskWritesInFlight++;
+
+ boolean hasListeners = mListeners.size() > 0;
+ if (hasListeners) {
+ keysModified = new ArrayList<String>();
+ listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
+ }
+
+ synchronized (mLock) {
+ boolean changesMade = false;
+
+ if (mClear) {
+ if (!mMap.isEmpty()) {
+ changesMade = true;
+ mMap.clear();
+ }
+ mClear = false;
+ }
+
+ for (Map.Entry<String, Object> e : mModified.entrySet()) {
+ String k = e.getKey();
+ Object v = e.getValue();
+ // "this" is the magic value for a removal mutation. In addition,
+ // setting a value to "null" for a given key is specified to be
+ // equivalent to calling remove on that key.
+ if (v == this || v == null) {
+ if (!mMap.containsKey(k)) {
+ continue;
+ }
+ mMap.remove(k);
+ } else {
+ if (mMap.containsKey(k)) {
+ Object existingValue = mMap.get(k);
+ if (existingValue != null && existingValue.equals(v)) {
+ continue;
+ }
+ }
+ mMap.put(k, v);
+ }
+
+ changesMade = true;
+ if (hasListeners) {
+ keysModified.add(k);
+ }
+ }
+
+ mModified.clear();
+
+ if (changesMade) {
+ mCurrentMemoryStateGeneration++;
+ }
+
+ memoryStateGeneration = mCurrentMemoryStateGeneration;
+ }
+ }
+ return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
+ mapToWriteToDisk);
+ }
+
+ public boolean commit() {
+ long startTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ MemoryCommitResult mcr = commitToMemory();
+
+ SharedPreferencesImpl.this.enqueueDiskWrite(
+ mcr, null /* sync write on this thread okay */);
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException e) {
+ return false;
+ } finally {
+ if (DEBUG) {
+ Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ + " committed after " + (System.currentTimeMillis() - startTime)
+ + " ms");
+ }
+ }
+ notifyListeners(mcr);
+ return mcr.writeToDiskResult;
+ }
+
+ private void notifyListeners(final MemoryCommitResult mcr) {
+ if (mcr.listeners == null || mcr.keysModified == null ||
+ mcr.keysModified.size() == 0) {
+ return;
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
+ final String key = mcr.keysModified.get(i);
+ for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
+ if (listener != null) {
+ listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
+ }
+ }
+ }
+ } else {
+ // Run this function on the main thread.
+ ActivityThread.sMainThreadHandler.post(new Runnable() {
+ public void run() {
+ notifyListeners(mcr);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Enqueue an already-committed-to-memory result to be written
+ * to disk.
+ *
+ * They will be written to disk one-at-a-time in the order
+ * that they're enqueued.
+ *
+ * @param postWriteRunnable if non-null, we're being called
+ * from apply() and this is the runnable to run after
+ * the write proceeds. if null (from a regular commit()),
+ * then we're allowed to do this disk write on the main
+ * thread (which in addition to reducing allocations and
+ * creating a background thread, this has the advantage that
+ * we catch them in userdebug StrictMode reports to convert
+ * them where possible to apply() ...)
+ */
+ private void enqueueDiskWrite(final MemoryCommitResult mcr,
+ final Runnable postWriteRunnable) {
+ final boolean isFromSyncCommit = (postWriteRunnable == null);
+
+ final Runnable writeToDiskRunnable = new Runnable() {
+ public void run() {
+ synchronized (mWritingToDiskLock) {
+ writeToFile(mcr, isFromSyncCommit);
+ }
+ synchronized (mLock) {
+ mDiskWritesInFlight--;
+ }
+ if (postWriteRunnable != null) {
+ postWriteRunnable.run();
+ }
+ }
+ };
+
+ // Typical #commit() path with fewer allocations, doing a write on
+ // the current thread.
+ if (isFromSyncCommit) {
+ boolean wasEmpty = false;
+ synchronized (mLock) {
+ wasEmpty = mDiskWritesInFlight == 1;
+ }
+ if (wasEmpty) {
+ writeToDiskRunnable.run();
+ return;
+ }
+ }
+
+ QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
+ }
+
+ private static FileOutputStream createFileOutputStream(File file) {
+ FileOutputStream str = null;
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e) {
+ File parent = file.getParentFile();
+ if (!parent.mkdir()) {
+ Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
+ return null;
+ }
+ FileUtils.setPermissions(
+ parent.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e2) {
+ Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
+ }
+ }
+ return str;
+ }
+
+ // Note: must hold mWritingToDiskLock
+ private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
+ long startTime = 0;
+ long existsTime = 0;
+ long backupExistsTime = 0;
+ long outputStreamCreateTime = 0;
+ long writeTime = 0;
+ long fsyncTime = 0;
+ long setPermTime = 0;
+ long fstatTime = 0;
+ long deleteTime = 0;
+
+ if (DEBUG) {
+ startTime = System.currentTimeMillis();
+ }
+
+ boolean fileExists = mFile.exists();
+
+ if (DEBUG) {
+ existsTime = System.currentTimeMillis();
+
+ // Might not be set, hence init them to a default value
+ backupExistsTime = existsTime;
+ }
+
+ // Rename the current file so it may be used as a backup during the next read
+ if (fileExists) {
+ boolean needsWrite = false;
+
+ // Only need to write if the disk state is older than this commit
+ if (mDiskStateGeneration < mcr.memoryStateGeneration) {
+ if (isFromSyncCommit) {
+ needsWrite = true;
+ } else {
+ synchronized (mLock) {
+ // No need to persist intermediate states. Just wait for the latest state to
+ // be persisted.
+ if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
+ needsWrite = true;
+ }
+ }
+ }
+ }
+
+ if (!needsWrite) {
+ mcr.setDiskWriteResult(false, true);
+ return;
+ }
+
+ boolean backupFileExists = mBackupFile.exists();
+
+ if (DEBUG) {
+ backupExistsTime = System.currentTimeMillis();
+ }
+
+ if (!backupFileExists) {
+ if (!mFile.renameTo(mBackupFile)) {
+ Log.e(TAG, "Couldn't rename file " + mFile
+ + " to backup file " + mBackupFile);
+ mcr.setDiskWriteResult(false, false);
+ return;
+ }
+ } else {
+ mFile.delete();
+ }
+ }
+
+ // Attempt to write the file, delete the backup and return true as atomically as
+ // possible. If any exception occurs, delete the new file; next time we will restore
+ // from the backup.
+ try {
+ FileOutputStream str = createFileOutputStream(mFile);
+
+ if (DEBUG) {
+ outputStreamCreateTime = System.currentTimeMillis();
+ }
+
+ if (str == null) {
+ mcr.setDiskWriteResult(false, false);
+ return;
+ }
+ XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
+
+ writeTime = System.currentTimeMillis();
+
+ FileUtils.sync(str);
+
+ fsyncTime = System.currentTimeMillis();
+
+ str.close();
+ ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
+
+ if (DEBUG) {
+ setPermTime = System.currentTimeMillis();
+ }
+
+ try {
+ final StructStat stat = Os.stat(mFile.getPath());
+ synchronized (mLock) {
+ mStatTimestamp = stat.st_mtim;
+ mStatSize = stat.st_size;
+ }
+ } catch (ErrnoException e) {
+ // Do nothing
+ }
+
+ if (DEBUG) {
+ fstatTime = System.currentTimeMillis();
+ }
+
+ // Writing was successful, delete the backup file if there is one.
+ mBackupFile.delete();
+
+ if (DEBUG) {
+ deleteTime = System.currentTimeMillis();
+ }
+
+ mDiskStateGeneration = mcr.memoryStateGeneration;
+
+ mcr.setDiskWriteResult(true, true);
+
+ if (DEBUG) {
+ Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ + (backupExistsTime - startTime) + "/"
+ + (outputStreamCreateTime - startTime) + "/"
+ + (writeTime - startTime) + "/"
+ + (fsyncTime - startTime) + "/"
+ + (setPermTime - startTime) + "/"
+ + (fstatTime - startTime) + "/"
+ + (deleteTime - startTime));
+ }
+
+ long fsyncDuration = fsyncTime - writeTime;
+ mSyncTimes.add((int) fsyncDuration);
+ mNumSync++;
+
+ if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
+ mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
+ }
+
+ return;
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "writeToFile: Got exception:", e);
+ } catch (IOException e) {
+ Log.w(TAG, "writeToFile: Got exception:", e);
+ }
+
+ // Clean up an unsuccessfully written file
+ if (mFile.exists()) {
+ if (!mFile.delete()) {
+ Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
+ }
+ }
+ mcr.setDiskWriteResult(false, false);
+ }
+}
diff --git a/android/app/StatusBarManager.java b/android/app/StatusBarManager.java
new file mode 100644
index 00000000..4a092140
--- /dev/null
+++ b/android/app/StatusBarManager.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.util.Slog;
+import android.view.View;
+
+import com.android.internal.statusbar.IStatusBarService;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Allows an app to control the status bar.
+ *
+ * @hide
+ */
+@SystemService(Context.STATUS_BAR_SERVICE)
+public class StatusBarManager {
+
+ public static final int DISABLE_EXPAND = View.STATUS_BAR_DISABLE_EXPAND;
+ public static final int DISABLE_NOTIFICATION_ICONS = View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS;
+ public static final int DISABLE_NOTIFICATION_ALERTS
+ = View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS;
+ @Deprecated
+ public static final int DISABLE_NOTIFICATION_TICKER
+ = View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER;
+ public static final int DISABLE_SYSTEM_INFO = View.STATUS_BAR_DISABLE_SYSTEM_INFO;
+ public static final int DISABLE_HOME = View.STATUS_BAR_DISABLE_HOME;
+ public static final int DISABLE_RECENT = View.STATUS_BAR_DISABLE_RECENT;
+ public static final int DISABLE_BACK = View.STATUS_BAR_DISABLE_BACK;
+ public static final int DISABLE_CLOCK = View.STATUS_BAR_DISABLE_CLOCK;
+ public static final int DISABLE_SEARCH = View.STATUS_BAR_DISABLE_SEARCH;
+
+ @Deprecated
+ public static final int DISABLE_NAVIGATION =
+ View.STATUS_BAR_DISABLE_HOME | View.STATUS_BAR_DISABLE_RECENT;
+
+ public static final int DISABLE_NONE = 0x00000000;
+
+ public static final int DISABLE_MASK = DISABLE_EXPAND | DISABLE_NOTIFICATION_ICONS
+ | DISABLE_NOTIFICATION_ALERTS | DISABLE_NOTIFICATION_TICKER
+ | DISABLE_SYSTEM_INFO | DISABLE_RECENT | DISABLE_HOME | DISABLE_BACK | DISABLE_CLOCK
+ | DISABLE_SEARCH;
+
+ /**
+ * Flag to disable quick settings.
+ *
+ * Setting this flag disables quick settings completely, but does not disable expanding the
+ * notification shade.
+ */
+ public static final int DISABLE2_QUICK_SETTINGS = 0x00000001;
+
+ public static final int DISABLE2_NONE = 0x00000000;
+
+ public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS;
+
+ @IntDef(flag = true,
+ value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Disable2Flags {}
+
+ public static final int NAVIGATION_HINT_BACK_ALT = 1 << 0;
+ public static final int NAVIGATION_HINT_IME_SHOWN = 1 << 1;
+
+ public static final int WINDOW_STATUS_BAR = 1;
+ public static final int WINDOW_NAVIGATION_BAR = 2;
+
+ public static final int WINDOW_STATE_SHOWING = 0;
+ public static final int WINDOW_STATE_HIDING = 1;
+ public static final int WINDOW_STATE_HIDDEN = 2;
+
+ public static final int CAMERA_LAUNCH_SOURCE_WIGGLE = 0;
+ public static final int CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = 1;
+ public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2;
+
+ private Context mContext;
+ private IStatusBarService mService;
+ private IBinder mToken = new Binder();
+
+ StatusBarManager(Context context) {
+ mContext = context;
+ }
+
+ private synchronized IStatusBarService getService() {
+ if (mService == null) {
+ mService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ if (mService == null) {
+ Slog.w("StatusBarManager", "warning: no STATUS_BAR_SERVICE");
+ }
+ }
+ return mService;
+ }
+
+ /**
+ * Disable some features in the status bar. Pass the bitwise-or of the DISABLE_* flags.
+ * To re-enable everything, pass {@link #DISABLE_NONE}.
+ */
+ public void disable(int what) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.disable(what, mToken, mContext.getPackageName());
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags.
+ * To re-enable everything, pass {@link #DISABLE_NONE}.
+ *
+ * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags.
+ */
+ public void disable2(@Disable2Flags int what) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.disable2(what, mToken, mContext.getPackageName());
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Expand the notifications panel.
+ */
+ public void expandNotificationsPanel() {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.expandNotificationsPanel();
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Collapse the notifications and settings panels.
+ */
+ public void collapsePanels() {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.collapsePanels();
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Expand the settings panel.
+ */
+ public void expandSettingsPanel() {
+ expandSettingsPanel(null);
+ }
+
+ /**
+ * Expand the settings panel and open a subPanel, pass null to just open the settings panel.
+ */
+ public void expandSettingsPanel(String subPanel) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.expandSettingsPanel(subPanel);
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ public void setIcon(String slot, int iconId, int iconLevel, String contentDescription) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.setIcon(slot, mContext.getPackageName(), iconId, iconLevel,
+ contentDescription);
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ public void removeIcon(String slot) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.removeIcon(slot);
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ public void setIconVisibility(String slot, boolean visible) {
+ try {
+ final IStatusBarService svc = getService();
+ if (svc != null) {
+ svc.setIconVisibility(slot, visible);
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public static String windowStateToString(int state) {
+ if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";
+ if (state == WINDOW_STATE_HIDDEN) return "WINDOW_STATE_HIDDEN";
+ if (state == WINDOW_STATE_SHOWING) return "WINDOW_STATE_SHOWING";
+ return "WINDOW_STATE_UNKNOWN";
+ }
+}
diff --git a/android/app/SynchronousUserSwitchObserver.java b/android/app/SynchronousUserSwitchObserver.java
new file mode 100644
index 00000000..3a738886
--- /dev/null
+++ b/android/app/SynchronousUserSwitchObserver.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app;
+
+import android.os.Bundle;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+
+/**
+ * Base class for synchronous implementations of {@link IUserSwitchObserver}
+ *
+ * @hide
+ */
+public abstract class SynchronousUserSwitchObserver extends UserSwitchObserver {
+ /**
+ * Calls {@link #onUserSwitching(int)} and notifies {@code reply} by calling
+ * {@link IRemoteCallback#sendResult(Bundle)}.
+ */
+ @Override
+ public final void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
+ try {
+ onUserSwitching(newUserId);
+ } finally {
+ if (reply != null) {
+ reply.sendResult(null);
+ }
+ }
+ }
+
+ /**
+ * Synchronous version of {@link IUserSwitchObserver#onUserSwitching(int, IRemoteCallback)}
+ */
+ public abstract void onUserSwitching(int newUserId) throws RemoteException;
+}
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
new file mode 100644
index 00000000..ab70f0e7
--- /dev/null
+++ b/android/app/SystemServiceRegistry.java
@@ -0,0 +1,1042 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.accounts.AccountManager;
+import android.accounts.IAccountManager;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.IDevicePolicyManager;
+import android.app.job.IJobScheduler;
+import android.app.job.JobScheduler;
+import android.app.timezone.RulesManager;
+import android.app.trust.TrustManager;
+import android.app.usage.IStorageStatsManager;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.NetworkStatsManager;
+import android.app.usage.StorageStatsManager;
+import android.app.usage.UsageStatsManager;
+import android.appwidget.AppWidgetManager;
+import android.bluetooth.BluetoothManager;
+import android.companion.CompanionDeviceManager;
+import android.companion.ICompanionDeviceManager;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.IRestrictionsManager;
+import android.content.RestrictionsManager;
+import android.content.pm.IShortcutService;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutManager;
+import android.content.res.Resources;
+import android.hardware.ConsumerIrManager;
+import android.hardware.ISerialManager;
+import android.hardware.SensorManager;
+import android.hardware.SerialManager;
+import android.hardware.SystemSensorManager;
+import android.hardware.camera2.CameraManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.IFingerprintService;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlService;
+import android.hardware.input.InputManager;
+import android.hardware.location.ContextHubManager;
+import android.hardware.radio.RadioManager;
+import android.hardware.usb.IUsbManager;
+import android.hardware.usb.UsbManager;
+import android.location.CountryDetector;
+import android.location.ICountryDetector;
+import android.location.ILocationManager;
+import android.location.LocationManager;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.media.midi.IMidiManager;
+import android.media.midi.MidiManager;
+import android.media.projection.MediaProjectionManager;
+import android.media.session.MediaSessionManager;
+import android.media.soundtrigger.SoundTriggerManager;
+import android.media.tv.ITvInputManager;
+import android.media.tv.TvInputManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityThread;
+import android.net.EthernetManager;
+import android.net.IConnectivityManager;
+import android.net.IEthernetManager;
+import android.net.IIpSecService;
+import android.net.INetworkPolicyManager;
+import android.net.IpSecManager;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkScoreManager;
+import android.net.nsd.INsdManager;
+import android.net.nsd.NsdManager;
+import android.net.lowpan.ILowpanManager;
+import android.net.lowpan.LowpanManager;
+import android.net.wifi.IRttManager;
+import android.net.wifi.IWifiManager;
+import android.net.wifi.IWifiScanner;
+import android.net.wifi.RttManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiScanner;
+import android.net.wifi.aware.IWifiAwareManager;
+import android.net.wifi.aware.WifiAwareManager;
+import android.net.wifi.p2p.IWifiP2pManager;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.nfc.NfcManager;
+import android.os.BatteryManager;
+import android.os.BatteryStats;
+import android.os.Build;
+import android.os.DropBoxManager;
+import android.os.HardwarePropertiesManager;
+import android.os.IBatteryPropertiesRegistrar;
+import android.os.IBinder;
+import android.os.IHardwarePropertiesManager;
+import android.os.IPowerManager;
+import android.os.IRecoverySystem;
+import android.os.IUserManager;
+import android.os.IncidentManager;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RecoverySystem;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.SystemVibrator;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.Vibrator;
+import android.os.health.SystemHealthManager;
+import android.os.storage.StorageManager;
+import android.print.IPrintManager;
+import android.print.PrintManager;
+import android.service.oemlock.IOemLockService;
+import android.service.oemlock.OemLockManager;
+import android.service.persistentdata.IPersistentDataBlockService;
+import android.service.persistentdata.PersistentDataBlockManager;
+import android.service.vr.IVrManager;
+import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccManager;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.CaptioningManager;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.IAutoFillManager;
+import android.view.inputmethod.InputMethodManager;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textservice.TextServicesManager;
+
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.appwidget.IAppWidgetService;
+import com.android.internal.os.IDropBoxManagerService;
+import com.android.internal.policy.PhoneLayoutInflater;
+
+import java.util.HashMap;
+
+/**
+ * Manages all of the system services that can be returned by {@link Context#getSystemService}.
+ * Used by {@link ContextImpl}.
+ */
+final class SystemServiceRegistry {
+ private static final String TAG = "SystemServiceRegistry";
+
+ // Service registry information.
+ // This information is never changed once static initialization has completed.
+ private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES =
+ new HashMap<Class<?>, String>();
+ private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
+ new HashMap<String, ServiceFetcher<?>>();
+ private static int sServiceCacheSize;
+
+ // Not instantiable.
+ private SystemServiceRegistry() { }
+
+ static {
+ registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class,
+ new CachedServiceFetcher<AccessibilityManager>() {
+ @Override
+ public AccessibilityManager createService(ContextImpl ctx) {
+ return AccessibilityManager.getInstance(ctx);
+ }});
+
+ registerService(Context.CAPTIONING_SERVICE, CaptioningManager.class,
+ new CachedServiceFetcher<CaptioningManager>() {
+ @Override
+ public CaptioningManager createService(ContextImpl ctx) {
+ return new CaptioningManager(ctx);
+ }});
+
+ registerService(Context.ACCOUNT_SERVICE, AccountManager.class,
+ new CachedServiceFetcher<AccountManager>() {
+ @Override
+ public AccountManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.ACCOUNT_SERVICE);
+ IAccountManager service = IAccountManager.Stub.asInterface(b);
+ return new AccountManager(ctx, service);
+ }});
+
+ registerService(Context.ACTIVITY_SERVICE, ActivityManager.class,
+ new CachedServiceFetcher<ActivityManager>() {
+ @Override
+ public ActivityManager createService(ContextImpl ctx) {
+ return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
+ }});
+
+ registerService(Context.ALARM_SERVICE, AlarmManager.class,
+ new CachedServiceFetcher<AlarmManager>() {
+ @Override
+ public AlarmManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.ALARM_SERVICE);
+ IAlarmManager service = IAlarmManager.Stub.asInterface(b);
+ return new AlarmManager(service, ctx);
+ }});
+
+ registerService(Context.AUDIO_SERVICE, AudioManager.class,
+ new CachedServiceFetcher<AudioManager>() {
+ @Override
+ public AudioManager createService(ContextImpl ctx) {
+ return new AudioManager(ctx);
+ }});
+
+ registerService(Context.MEDIA_ROUTER_SERVICE, MediaRouter.class,
+ new CachedServiceFetcher<MediaRouter>() {
+ @Override
+ public MediaRouter createService(ContextImpl ctx) {
+ return new MediaRouter(ctx);
+ }});
+
+ registerService(Context.BLUETOOTH_SERVICE, BluetoothManager.class,
+ new CachedServiceFetcher<BluetoothManager>() {
+ @Override
+ public BluetoothManager createService(ContextImpl ctx) {
+ return new BluetoothManager(ctx);
+ }});
+
+ registerService(Context.HDMI_CONTROL_SERVICE, HdmiControlManager.class,
+ new StaticServiceFetcher<HdmiControlManager>() {
+ @Override
+ public HdmiControlManager createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.HDMI_CONTROL_SERVICE);
+ return new HdmiControlManager(IHdmiControlService.Stub.asInterface(b));
+ }});
+
+ registerService(Context.TEXT_CLASSIFICATION_SERVICE, TextClassificationManager.class,
+ new CachedServiceFetcher<TextClassificationManager>() {
+ @Override
+ public TextClassificationManager createService(ContextImpl ctx) {
+ return new TextClassificationManager(ctx);
+ }});
+
+ registerService(Context.CLIPBOARD_SERVICE, ClipboardManager.class,
+ new CachedServiceFetcher<ClipboardManager>() {
+ @Override
+ public ClipboardManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new ClipboardManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler());
+ }});
+
+ // The clipboard service moved to a new package. If someone asks for the old
+ // interface by class then we want to redirect over to the new interface instead
+ // (which extends it).
+ SYSTEM_SERVICE_NAMES.put(android.text.ClipboardManager.class, Context.CLIPBOARD_SERVICE);
+
+ registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class,
+ new StaticApplicationContextServiceFetcher<ConnectivityManager>() {
+ @Override
+ public ConnectivityManager createService(Context context) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.CONNECTIVITY_SERVICE);
+ IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);
+ return new ConnectivityManager(context, service);
+ }});
+
+ registerService(Context.IPSEC_SERVICE, IpSecManager.class,
+ new StaticServiceFetcher<IpSecManager>() {
+ @Override
+ public IpSecManager createService() {
+ IBinder b = ServiceManager.getService(Context.IPSEC_SERVICE);
+ IIpSecService service = IIpSecService.Stub.asInterface(b);
+ return new IpSecManager(service);
+ }});
+
+ registerService(Context.COUNTRY_DETECTOR, CountryDetector.class,
+ new StaticServiceFetcher<CountryDetector>() {
+ @Override
+ public CountryDetector createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.COUNTRY_DETECTOR);
+ return new CountryDetector(ICountryDetector.Stub.asInterface(b));
+ }});
+
+ registerService(Context.DEVICE_POLICY_SERVICE, DevicePolicyManager.class,
+ new CachedServiceFetcher<DevicePolicyManager>() {
+ @Override
+ public DevicePolicyManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.DEVICE_POLICY_SERVICE);
+ return new DevicePolicyManager(ctx, IDevicePolicyManager.Stub.asInterface(b));
+ }});
+
+ registerService(Context.DOWNLOAD_SERVICE, DownloadManager.class,
+ new CachedServiceFetcher<DownloadManager>() {
+ @Override
+ public DownloadManager createService(ContextImpl ctx) {
+ return new DownloadManager(ctx);
+ }});
+
+ registerService(Context.BATTERY_SERVICE, BatteryManager.class,
+ new StaticServiceFetcher<BatteryManager>() {
+ @Override
+ public BatteryManager createService() throws ServiceNotFoundException {
+ IBatteryStats stats = IBatteryStats.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME));
+ IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub
+ .asInterface(ServiceManager.getServiceOrThrow("batteryproperties"));
+ return new BatteryManager(stats, registrar);
+ }});
+
+ registerService(Context.NFC_SERVICE, NfcManager.class,
+ new CachedServiceFetcher<NfcManager>() {
+ @Override
+ public NfcManager createService(ContextImpl ctx) {
+ return new NfcManager(ctx);
+ }});
+
+ registerService(Context.DROPBOX_SERVICE, DropBoxManager.class,
+ new CachedServiceFetcher<DropBoxManager>() {
+ @Override
+ public DropBoxManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.DROPBOX_SERVICE);
+ IDropBoxManagerService service = IDropBoxManagerService.Stub.asInterface(b);
+ return new DropBoxManager(ctx, service);
+ }});
+
+ registerService(Context.INPUT_SERVICE, InputManager.class,
+ new StaticServiceFetcher<InputManager>() {
+ @Override
+ public InputManager createService() {
+ return InputManager.getInstance();
+ }});
+
+ registerService(Context.DISPLAY_SERVICE, DisplayManager.class,
+ new CachedServiceFetcher<DisplayManager>() {
+ @Override
+ public DisplayManager createService(ContextImpl ctx) {
+ return new DisplayManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.INPUT_METHOD_SERVICE, InputMethodManager.class,
+ new StaticServiceFetcher<InputMethodManager>() {
+ @Override
+ public InputMethodManager createService() {
+ return InputMethodManager.getInstance();
+ }});
+
+ registerService(Context.TEXT_SERVICES_MANAGER_SERVICE, TextServicesManager.class,
+ new StaticServiceFetcher<TextServicesManager>() {
+ @Override
+ public TextServicesManager createService() {
+ return TextServicesManager.getInstance();
+ }});
+
+ registerService(Context.KEYGUARD_SERVICE, KeyguardManager.class,
+ new CachedServiceFetcher<KeyguardManager>() {
+ @Override
+ public KeyguardManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new KeyguardManager(ctx);
+ }});
+
+ registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
+ new CachedServiceFetcher<LayoutInflater>() {
+ @Override
+ public LayoutInflater createService(ContextImpl ctx) {
+ return new PhoneLayoutInflater(ctx.getOuterContext());
+ }});
+
+ registerService(Context.LOCATION_SERVICE, LocationManager.class,
+ new CachedServiceFetcher<LocationManager>() {
+ @Override
+ public LocationManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.LOCATION_SERVICE);
+ return new LocationManager(ctx, ILocationManager.Stub.asInterface(b));
+ }});
+
+ registerService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class,
+ new CachedServiceFetcher<NetworkPolicyManager>() {
+ @Override
+ public NetworkPolicyManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new NetworkPolicyManager(ctx, INetworkPolicyManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.NETWORK_POLICY_SERVICE)));
+ }});
+
+ registerService(Context.NOTIFICATION_SERVICE, NotificationManager.class,
+ new CachedServiceFetcher<NotificationManager>() {
+ @Override
+ public NotificationManager createService(ContextImpl ctx) {
+ final Context outerContext = ctx.getOuterContext();
+ return new NotificationManager(
+ new ContextThemeWrapper(outerContext,
+ Resources.selectSystemTheme(0,
+ outerContext.getApplicationInfo().targetSdkVersion,
+ com.android.internal.R.style.Theme_Dialog,
+ com.android.internal.R.style.Theme_Holo_Dialog,
+ com.android.internal.R.style.Theme_DeviceDefault_Dialog,
+ com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)),
+ ctx.mMainThread.getHandler());
+ }});
+
+ registerService(Context.NSD_SERVICE, NsdManager.class,
+ new CachedServiceFetcher<NsdManager>() {
+ @Override
+ public NsdManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.NSD_SERVICE);
+ INsdManager service = INsdManager.Stub.asInterface(b);
+ return new NsdManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.POWER_SERVICE, PowerManager.class,
+ new CachedServiceFetcher<PowerManager>() {
+ @Override
+ public PowerManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.POWER_SERVICE);
+ IPowerManager service = IPowerManager.Stub.asInterface(b);
+ return new PowerManager(ctx.getOuterContext(),
+ service, ctx.mMainThread.getHandler());
+ }});
+
+ registerService(Context.RECOVERY_SERVICE, RecoverySystem.class,
+ new CachedServiceFetcher<RecoverySystem>() {
+ @Override
+ public RecoverySystem createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.RECOVERY_SERVICE);
+ IRecoverySystem service = IRecoverySystem.Stub.asInterface(b);
+ return new RecoverySystem(service);
+ }});
+
+ registerService(Context.SEARCH_SERVICE, SearchManager.class,
+ new CachedServiceFetcher<SearchManager>() {
+ @Override
+ public SearchManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new SearchManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler());
+ }});
+
+ registerService(Context.SENSOR_SERVICE, SensorManager.class,
+ new CachedServiceFetcher<SensorManager>() {
+ @Override
+ public SensorManager createService(ContextImpl ctx) {
+ return new SystemSensorManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler().getLooper());
+ }});
+
+ registerService(Context.STATUS_BAR_SERVICE, StatusBarManager.class,
+ new CachedServiceFetcher<StatusBarManager>() {
+ @Override
+ public StatusBarManager createService(ContextImpl ctx) {
+ return new StatusBarManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.STORAGE_SERVICE, StorageManager.class,
+ new CachedServiceFetcher<StorageManager>() {
+ @Override
+ public StorageManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new StorageManager(ctx, ctx.mMainThread.getHandler().getLooper());
+ }});
+
+ registerService(Context.STORAGE_STATS_SERVICE, StorageStatsManager.class,
+ new CachedServiceFetcher<StorageStatsManager>() {
+ @Override
+ public StorageStatsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IStorageStatsManager service = IStorageStatsManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.STORAGE_STATS_SERVICE));
+ return new StorageStatsManager(ctx, service);
+ }});
+
+ registerService(Context.TELEPHONY_SERVICE, TelephonyManager.class,
+ new CachedServiceFetcher<TelephonyManager>() {
+ @Override
+ public TelephonyManager createService(ContextImpl ctx) {
+ return new TelephonyManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class,
+ new CachedServiceFetcher<SubscriptionManager>() {
+ @Override
+ public SubscriptionManager createService(ContextImpl ctx) {
+ return new SubscriptionManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.CARRIER_CONFIG_SERVICE, CarrierConfigManager.class,
+ new CachedServiceFetcher<CarrierConfigManager>() {
+ @Override
+ public CarrierConfigManager createService(ContextImpl ctx) {
+ return new CarrierConfigManager();
+ }});
+
+ registerService(Context.TELECOM_SERVICE, TelecomManager.class,
+ new CachedServiceFetcher<TelecomManager>() {
+ @Override
+ public TelecomManager createService(ContextImpl ctx) {
+ return new TelecomManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.EUICC_SERVICE, EuiccManager.class,
+ new CachedServiceFetcher<EuiccManager>() {
+ @Override
+ public EuiccManager createService(ContextImpl ctx) {
+ return new EuiccManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.UI_MODE_SERVICE, UiModeManager.class,
+ new CachedServiceFetcher<UiModeManager>() {
+ @Override
+ public UiModeManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new UiModeManager();
+ }});
+
+ registerService(Context.USB_SERVICE, UsbManager.class,
+ new CachedServiceFetcher<UsbManager>() {
+ @Override
+ public UsbManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.USB_SERVICE);
+ return new UsbManager(ctx, IUsbManager.Stub.asInterface(b));
+ }});
+
+ registerService(Context.SERIAL_SERVICE, SerialManager.class,
+ new CachedServiceFetcher<SerialManager>() {
+ @Override
+ public SerialManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.SERIAL_SERVICE);
+ return new SerialManager(ctx, ISerialManager.Stub.asInterface(b));
+ }});
+
+ registerService(Context.VIBRATOR_SERVICE, Vibrator.class,
+ new CachedServiceFetcher<Vibrator>() {
+ @Override
+ public Vibrator createService(ContextImpl ctx) {
+ return new SystemVibrator(ctx);
+ }});
+
+ registerService(Context.WALLPAPER_SERVICE, WallpaperManager.class,
+ new CachedServiceFetcher<WallpaperManager>() {
+ @Override
+ public WallpaperManager createService(ContextImpl ctx) {
+ return new WallpaperManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler());
+ }});
+
+ registerService(Context.LOWPAN_SERVICE, LowpanManager.class,
+ new CachedServiceFetcher<LowpanManager>() {
+ @Override
+ public LowpanManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.LOWPAN_SERVICE);
+ ILowpanManager service = ILowpanManager.Stub.asInterface(b);
+ return new LowpanManager(ctx.getOuterContext(), service,
+ ConnectivityThread.getInstanceLooper());
+ }});
+
+ registerService(Context.WIFI_SERVICE, WifiManager.class,
+ new CachedServiceFetcher<WifiManager>() {
+ @Override
+ public WifiManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_SERVICE);
+ IWifiManager service = IWifiManager.Stub.asInterface(b);
+ return new WifiManager(ctx.getOuterContext(), service,
+ ConnectivityThread.getInstanceLooper());
+ }});
+
+ registerService(Context.WIFI_P2P_SERVICE, WifiP2pManager.class,
+ new StaticServiceFetcher<WifiP2pManager>() {
+ @Override
+ public WifiP2pManager createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_P2P_SERVICE);
+ IWifiP2pManager service = IWifiP2pManager.Stub.asInterface(b);
+ return new WifiP2pManager(service);
+ }});
+
+ registerService(Context.WIFI_AWARE_SERVICE, WifiAwareManager.class,
+ new CachedServiceFetcher<WifiAwareManager>() {
+ @Override
+ public WifiAwareManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_AWARE_SERVICE);
+ IWifiAwareManager service = IWifiAwareManager.Stub.asInterface(b);
+ if (service == null) {
+ return null;
+ }
+ return new WifiAwareManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.WIFI_SCANNING_SERVICE, WifiScanner.class,
+ new CachedServiceFetcher<WifiScanner>() {
+ @Override
+ public WifiScanner createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_SCANNING_SERVICE);
+ IWifiScanner service = IWifiScanner.Stub.asInterface(b);
+ return new WifiScanner(ctx.getOuterContext(), service,
+ ConnectivityThread.getInstanceLooper());
+ }});
+
+ registerService(Context.WIFI_RTT_SERVICE, RttManager.class,
+ new CachedServiceFetcher<RttManager>() {
+ @Override
+ public RttManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_RTT_SERVICE);
+ IRttManager service = IRttManager.Stub.asInterface(b);
+ return new RttManager(ctx.getOuterContext(), service,
+ ConnectivityThread.getInstanceLooper());
+ }});
+
+ registerService(Context.ETHERNET_SERVICE, EthernetManager.class,
+ new CachedServiceFetcher<EthernetManager>() {
+ @Override
+ public EthernetManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.ETHERNET_SERVICE);
+ IEthernetManager service = IEthernetManager.Stub.asInterface(b);
+ return new EthernetManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.WINDOW_SERVICE, WindowManager.class,
+ new CachedServiceFetcher<WindowManager>() {
+ @Override
+ public WindowManager createService(ContextImpl ctx) {
+ return new WindowManagerImpl(ctx);
+ }});
+
+ registerService(Context.USER_SERVICE, UserManager.class,
+ new CachedServiceFetcher<UserManager>() {
+ @Override
+ public UserManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.USER_SERVICE);
+ IUserManager service = IUserManager.Stub.asInterface(b);
+ return new UserManager(ctx, service);
+ }});
+
+ registerService(Context.APP_OPS_SERVICE, AppOpsManager.class,
+ new CachedServiceFetcher<AppOpsManager>() {
+ @Override
+ public AppOpsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.APP_OPS_SERVICE);
+ IAppOpsService service = IAppOpsService.Stub.asInterface(b);
+ return new AppOpsManager(ctx, service);
+ }});
+
+ registerService(Context.CAMERA_SERVICE, CameraManager.class,
+ new CachedServiceFetcher<CameraManager>() {
+ @Override
+ public CameraManager createService(ContextImpl ctx) {
+ return new CameraManager(ctx);
+ }});
+
+ registerService(Context.LAUNCHER_APPS_SERVICE, LauncherApps.class,
+ new CachedServiceFetcher<LauncherApps>() {
+ @Override
+ public LauncherApps createService(ContextImpl ctx) {
+ return new LauncherApps(ctx);
+ }});
+
+ registerService(Context.RESTRICTIONS_SERVICE, RestrictionsManager.class,
+ new CachedServiceFetcher<RestrictionsManager>() {
+ @Override
+ public RestrictionsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.RESTRICTIONS_SERVICE);
+ IRestrictionsManager service = IRestrictionsManager.Stub.asInterface(b);
+ return new RestrictionsManager(ctx, service);
+ }});
+
+ registerService(Context.PRINT_SERVICE, PrintManager.class,
+ new CachedServiceFetcher<PrintManager>() {
+ @Override
+ public PrintManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IPrintManager service = null;
+ // If the feature not present, don't try to look up every time
+ if (ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PRINTING)) {
+ service = IPrintManager.Stub.asInterface(ServiceManager
+ .getServiceOrThrow(Context.PRINT_SERVICE));
+ }
+ return new PrintManager(ctx.getOuterContext(), service, UserHandle.myUserId(),
+ UserHandle.getAppId(Process.myUid()));
+ }});
+
+ registerService(Context.COMPANION_DEVICE_SERVICE, CompanionDeviceManager.class,
+ new CachedServiceFetcher<CompanionDeviceManager>() {
+ @Override
+ public CompanionDeviceManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ ICompanionDeviceManager service = null;
+ // If the feature not present, don't try to look up every time
+ if (ctx.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_COMPANION_DEVICE_SETUP)) {
+ service = ICompanionDeviceManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.COMPANION_DEVICE_SERVICE));
+ }
+ return new CompanionDeviceManager(service, ctx.getOuterContext());
+ }});
+
+ registerService(Context.CONSUMER_IR_SERVICE, ConsumerIrManager.class,
+ new CachedServiceFetcher<ConsumerIrManager>() {
+ @Override
+ public ConsumerIrManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new ConsumerIrManager(ctx);
+ }});
+
+ registerService(Context.MEDIA_SESSION_SERVICE, MediaSessionManager.class,
+ new CachedServiceFetcher<MediaSessionManager>() {
+ @Override
+ public MediaSessionManager createService(ContextImpl ctx) {
+ return new MediaSessionManager(ctx);
+ }});
+
+ registerService(Context.TRUST_SERVICE, TrustManager.class,
+ new StaticServiceFetcher<TrustManager>() {
+ @Override
+ public TrustManager createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.TRUST_SERVICE);
+ return new TrustManager(b);
+ }});
+
+ registerService(Context.FINGERPRINT_SERVICE, FingerprintManager.class,
+ new CachedServiceFetcher<FingerprintManager>() {
+ @Override
+ public FingerprintManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ final IBinder binder;
+ if (ctx.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) {
+ binder = ServiceManager.getServiceOrThrow(Context.FINGERPRINT_SERVICE);
+ } else {
+ binder = ServiceManager.getService(Context.FINGERPRINT_SERVICE);
+ }
+ IFingerprintService service = IFingerprintService.Stub.asInterface(binder);
+ return new FingerprintManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.TV_INPUT_SERVICE, TvInputManager.class,
+ new StaticServiceFetcher<TvInputManager>() {
+ @Override
+ public TvInputManager createService() throws ServiceNotFoundException {
+ IBinder iBinder = ServiceManager.getServiceOrThrow(Context.TV_INPUT_SERVICE);
+ ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder);
+ return new TvInputManager(service, UserHandle.myUserId());
+ }});
+
+ registerService(Context.NETWORK_SCORE_SERVICE, NetworkScoreManager.class,
+ new CachedServiceFetcher<NetworkScoreManager>() {
+ @Override
+ public NetworkScoreManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new NetworkScoreManager(ctx);
+ }});
+
+ registerService(Context.USAGE_STATS_SERVICE, UsageStatsManager.class,
+ new CachedServiceFetcher<UsageStatsManager>() {
+ @Override
+ public UsageStatsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder iBinder = ServiceManager.getServiceOrThrow(Context.USAGE_STATS_SERVICE);
+ IUsageStatsManager service = IUsageStatsManager.Stub.asInterface(iBinder);
+ return new UsageStatsManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class,
+ new CachedServiceFetcher<NetworkStatsManager>() {
+ @Override
+ public NetworkStatsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new NetworkStatsManager(ctx.getOuterContext());
+ }});
+
+ registerService(Context.JOB_SCHEDULER_SERVICE, JobScheduler.class,
+ new StaticServiceFetcher<JobScheduler>() {
+ @Override
+ public JobScheduler createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.JOB_SCHEDULER_SERVICE);
+ return new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b));
+ }});
+
+ registerService(Context.PERSISTENT_DATA_BLOCK_SERVICE, PersistentDataBlockManager.class,
+ new StaticServiceFetcher<PersistentDataBlockManager>() {
+ @Override
+ public PersistentDataBlockManager createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.PERSISTENT_DATA_BLOCK_SERVICE);
+ IPersistentDataBlockService persistentDataBlockService =
+ IPersistentDataBlockService.Stub.asInterface(b);
+ if (persistentDataBlockService != null) {
+ return new PersistentDataBlockManager(persistentDataBlockService);
+ } else {
+ // not supported
+ return null;
+ }
+ }});
+
+ registerService(Context.OEM_LOCK_SERVICE, OemLockManager.class,
+ new StaticServiceFetcher<OemLockManager>() {
+ @Override
+ public OemLockManager createService() throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.OEM_LOCK_SERVICE);
+ IOemLockService oemLockService = IOemLockService.Stub.asInterface(b);
+ if (oemLockService != null) {
+ return new OemLockManager(oemLockService);
+ } else {
+ // not supported
+ return null;
+ }
+ }});
+
+ registerService(Context.MEDIA_PROJECTION_SERVICE, MediaProjectionManager.class,
+ new CachedServiceFetcher<MediaProjectionManager>() {
+ @Override
+ public MediaProjectionManager createService(ContextImpl ctx) {
+ return new MediaProjectionManager(ctx);
+ }});
+
+ registerService(Context.APPWIDGET_SERVICE, AppWidgetManager.class,
+ new CachedServiceFetcher<AppWidgetManager>() {
+ @Override
+ public AppWidgetManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.APPWIDGET_SERVICE);
+ return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b));
+ }});
+
+ registerService(Context.MIDI_SERVICE, MidiManager.class,
+ new CachedServiceFetcher<MidiManager>() {
+ @Override
+ public MidiManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.MIDI_SERVICE);
+ return new MidiManager(IMidiManager.Stub.asInterface(b));
+ }});
+
+ registerService(Context.RADIO_SERVICE, RadioManager.class,
+ new CachedServiceFetcher<RadioManager>() {
+ @Override
+ public RadioManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new RadioManager(ctx);
+ }});
+
+ registerService(Context.HARDWARE_PROPERTIES_SERVICE, HardwarePropertiesManager.class,
+ new CachedServiceFetcher<HardwarePropertiesManager>() {
+ @Override
+ public HardwarePropertiesManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.HARDWARE_PROPERTIES_SERVICE);
+ IHardwarePropertiesManager service =
+ IHardwarePropertiesManager.Stub.asInterface(b);
+ return new HardwarePropertiesManager(ctx, service);
+ }});
+
+ registerService(Context.SOUND_TRIGGER_SERVICE, SoundTriggerManager.class,
+ new CachedServiceFetcher<SoundTriggerManager>() {
+ @Override
+ public SoundTriggerManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.SOUND_TRIGGER_SERVICE);
+ return new SoundTriggerManager(ctx, ISoundTriggerService.Stub.asInterface(b));
+ }});
+
+ registerService(Context.SHORTCUT_SERVICE, ShortcutManager.class,
+ new CachedServiceFetcher<ShortcutManager>() {
+ @Override
+ public ShortcutManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.SHORTCUT_SERVICE);
+ return new ShortcutManager(ctx, IShortcutService.Stub.asInterface(b));
+ }});
+
+ registerService(Context.SYSTEM_HEALTH_SERVICE, SystemHealthManager.class,
+ new CachedServiceFetcher<SystemHealthManager>() {
+ @Override
+ public SystemHealthManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME);
+ return new SystemHealthManager(IBatteryStats.Stub.asInterface(b));
+ }});
+
+ registerService(Context.CONTEXTHUB_SERVICE, ContextHubManager.class,
+ new CachedServiceFetcher<ContextHubManager>() {
+ @Override
+ public ContextHubManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new ContextHubManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler().getLooper());
+ }});
+
+ registerService(Context.INCIDENT_SERVICE, IncidentManager.class,
+ new CachedServiceFetcher<IncidentManager>() {
+ @Override
+ public IncidentManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new IncidentManager(ctx);
+ }});
+
+ registerService(Context.AUTOFILL_MANAGER_SERVICE, AutofillManager.class,
+ new CachedServiceFetcher<AutofillManager>() {
+ @Override
+ public AutofillManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ // Get the services without throwing as this is an optional feature
+ IBinder b = ServiceManager.getService(Context.AUTOFILL_MANAGER_SERVICE);
+ IAutoFillManager service = IAutoFillManager.Stub.asInterface(b);
+ return new AutofillManager(ctx.getOuterContext(), service);
+ }});
+
+ registerService(Context.VR_SERVICE, VrManager.class, new CachedServiceFetcher<VrManager>() {
+ @Override
+ public VrManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.VR_SERVICE);
+ return new VrManager(IVrManager.Stub.asInterface(b));
+ }
+ });
+
+ registerService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, RulesManager.class,
+ new CachedServiceFetcher<RulesManager>() {
+ @Override
+ public RulesManager createService(ContextImpl ctx) {
+ return new RulesManager(ctx.getOuterContext());
+ }});
+ }
+
+ /**
+ * Creates an array which is used to cache per-Context service instances.
+ */
+ public static Object[] createServiceCache() {
+ return new Object[sServiceCacheSize];
+ }
+
+ /**
+ * Gets a system service from a given context.
+ */
+ public static Object getSystemService(ContextImpl ctx, String name) {
+ ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
+ return fetcher != null ? fetcher.getService(ctx) : null;
+ }
+
+ /**
+ * Gets the name of the system-level service that is represented by the specified class.
+ */
+ public static String getSystemServiceName(Class<?> serviceClass) {
+ return SYSTEM_SERVICE_NAMES.get(serviceClass);
+ }
+
+ /**
+ * Statically registers a system service with the context.
+ * This method must be called during static initialization only.
+ */
+ private static <T> void registerService(String serviceName, Class<T> serviceClass,
+ ServiceFetcher<T> serviceFetcher) {
+ SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
+ SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
+ }
+
+ /**
+ * Base interface for classes that fetch services.
+ * These objects must only be created during static initialization.
+ */
+ static abstract interface ServiceFetcher<T> {
+ T getService(ContextImpl ctx);
+ }
+
+ /**
+ * Override this class when the system service constructor needs a
+ * ContextImpl and should be cached and retained by that context.
+ */
+ static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
+ private final int mCacheIndex;
+
+ public CachedServiceFetcher() {
+ mCacheIndex = sServiceCacheSize++;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public final T getService(ContextImpl ctx) {
+ final Object[] cache = ctx.mServiceCache;
+ synchronized (cache) {
+ // Fetch or create the service.
+ Object service = cache[mCacheIndex];
+ if (service == null) {
+ try {
+ service = createService(ctx);
+ cache[mCacheIndex] = service;
+ } catch (ServiceNotFoundException e) {
+ onServiceNotFound(e);
+ }
+ }
+ return (T)service;
+ }
+ }
+
+ public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
+ }
+
+ /**
+ * Override this class when the system service does not need a ContextImpl
+ * and should be cached and retained process-wide.
+ */
+ static abstract class StaticServiceFetcher<T> implements ServiceFetcher<T> {
+ private T mCachedInstance;
+
+ @Override
+ public final T getService(ContextImpl ctx) {
+ synchronized (StaticServiceFetcher.this) {
+ if (mCachedInstance == null) {
+ try {
+ mCachedInstance = createService();
+ } catch (ServiceNotFoundException e) {
+ onServiceNotFound(e);
+ }
+ }
+ return mCachedInstance;
+ }
+ }
+
+ public abstract T createService() throws ServiceNotFoundException;
+ }
+
+ /**
+ * Like StaticServiceFetcher, creates only one instance of the service per application, but when
+ * creating the service for the first time, passes it the application context of the creating
+ * application.
+ *
+ * TODO: Delete this once its only user (ConnectivityManager) is known to work well in the
+ * case where multiple application components each have their own ConnectivityManager object.
+ */
+ static abstract class StaticApplicationContextServiceFetcher<T> implements ServiceFetcher<T> {
+ private T mCachedInstance;
+
+ @Override
+ public final T getService(ContextImpl ctx) {
+ synchronized (StaticApplicationContextServiceFetcher.this) {
+ if (mCachedInstance == null) {
+ Context appContext = ctx.getApplicationContext();
+ // If the application context is null, we're either in the system process or
+ // it's the application context very early in app initialization. In both these
+ // cases, the passed-in ContextImpl will not be freed, so it's safe to pass it
+ // to the service. http://b/27532714 .
+ try {
+ mCachedInstance = createService(appContext != null ? appContext : ctx);
+ } catch (ServiceNotFoundException e) {
+ onServiceNotFound(e);
+ }
+ }
+ return mCachedInstance;
+ }
+ }
+
+ public abstract T createService(Context applicationContext) throws ServiceNotFoundException;
+ }
+
+ public static void onServiceNotFound(ServiceNotFoundException e) {
+ // We're mostly interested in tracking down long-lived core system
+ // components that might stumble if they obtain bad references; just
+ // emit a tidy log message for normal apps
+ if (android.os.Process.myUid() < android.os.Process.FIRST_APPLICATION_UID) {
+ Log.wtf(TAG, e.getMessage(), e);
+ } else {
+ Log.w(TAG, e.getMessage());
+ }
+ }
+}
diff --git a/android/app/SystemServiceRegistry_Accessor.java b/android/app/SystemServiceRegistry_Accessor.java
new file mode 100644
index 00000000..591aee07
--- /dev/null
+++ b/android/app/SystemServiceRegistry_Accessor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+/**
+ * Class to allow accessing {@link SystemServiceRegistry#getSystemServiceName}
+ */
+public class SystemServiceRegistry_Accessor {
+ /**
+ * Gets the name of the system-level service that is represented by the specified class.
+ */
+ public static String getSystemServiceName(Class<?> serviceClass) {
+ return SystemServiceRegistry.getSystemServiceName(serviceClass);
+ }
+}
diff --git a/android/app/TabActivity.java b/android/app/TabActivity.java
new file mode 100644
index 00000000..ad8b0dbe
--- /dev/null
+++ b/android/app/TabActivity.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+
+/**
+ * <p>For apps developing against {@link android.os.Build.VERSION_CODES#HONEYCOMB}
+ * or later, tabs are typically presented in the UI using the new
+ * {@link ActionBar#newTab() ActionBar.newTab()} and
+ * related APIs for placing tabs within their action bar area.</p>
+ *
+ * @deprecated New applications should use Fragments instead of this class;
+ * to continue to run on older devices, you can use the v4 support library
+ * which provides a version of the Fragment API that is compatible down to
+ * {@link android.os.Build.VERSION_CODES#DONUT}.
+ */
+@Deprecated
+public class TabActivity extends ActivityGroup {
+ private TabHost mTabHost;
+ private String mDefaultTab = null;
+ private int mDefaultTabIndex = -1;
+
+ public TabActivity() {
+ }
+
+ /**
+ * Sets the default tab that is the first tab highlighted.
+ *
+ * @param tag the name of the default tab
+ */
+ public void setDefaultTab(String tag) {
+ mDefaultTab = tag;
+ mDefaultTabIndex = -1;
+ }
+
+ /**
+ * Sets the default tab that is the first tab highlighted.
+ *
+ * @param index the index of the default tab
+ */
+ public void setDefaultTab(int index) {
+ mDefaultTab = null;
+ mDefaultTabIndex = index;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+ ensureTabHost();
+ String cur = state.getString("currentTab");
+ if (cur != null) {
+ mTabHost.setCurrentTabByTag(cur);
+ }
+ if (mTabHost.getCurrentTab() < 0) {
+ if (mDefaultTab != null) {
+ mTabHost.setCurrentTabByTag(mDefaultTab);
+ } else if (mDefaultTabIndex >= 0) {
+ mTabHost.setCurrentTab(mDefaultTabIndex);
+ }
+ }
+ }
+
+ @Override
+ protected void onPostCreate(Bundle icicle) {
+ super.onPostCreate(icicle);
+
+ ensureTabHost();
+
+ if (mTabHost.getCurrentTab() == -1) {
+ mTabHost.setCurrentTab(0);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ String currentTabTag = mTabHost.getCurrentTabTag();
+ if (currentTabTag != null) {
+ outState.putString("currentTab", currentTabTag);
+ }
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ *@see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ mTabHost = findViewById(com.android.internal.R.id.tabhost);
+
+ if (mTabHost == null) {
+ throw new RuntimeException(
+ "Your content must have a TabHost whose id attribute is " +
+ "'android.R.id.tabhost'");
+ }
+ mTabHost.setup(getLocalActivityManager());
+ }
+
+ private void ensureTabHost() {
+ if (mTabHost == null) {
+ this.setContentView(com.android.internal.R.layout.tab_content);
+ }
+ }
+
+ @Override
+ protected void
+ onChildTitleChanged(Activity childActivity, CharSequence title) {
+ // Dorky implementation until we can have multiple activities running.
+ if (getLocalActivityManager().getCurrentActivity() == childActivity) {
+ View tabView = mTabHost.getCurrentTabView();
+ if (tabView != null && tabView instanceof TextView) {
+ ((TextView) tabView).setText(title);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link TabHost} the activity is using to host its tabs.
+ *
+ * @return the {@link TabHost} the activity is using to host its tabs.
+ */
+ public TabHost getTabHost() {
+ ensureTabHost();
+ return mTabHost;
+ }
+
+ /**
+ * Returns the {@link TabWidget} the activity is using to draw the actual tabs.
+ *
+ * @return the {@link TabWidget} the activity is using to draw the actual tabs.
+ */
+ public TabWidget getTabWidget() {
+ return mTabHost.getTabWidget();
+ }
+}
diff --git a/android/app/TaskStackBuilder.java b/android/app/TaskStackBuilder.java
new file mode 100644
index 00000000..bab993f8
--- /dev/null
+++ b/android/app/TaskStackBuilder.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class for constructing synthetic back stacks for cross-task navigation
+ * on Android 3.0 and newer.
+ *
+ * <p>In API level 11 (Android 3.0/Honeycomb) the recommended conventions for
+ * app navigation using the back key changed. The back key's behavior is local
+ * to the current task and does not capture navigation across different tasks.
+ * Navigating across tasks and easily reaching the previous task is accomplished
+ * through the "recents" UI, accessible through the software-provided Recents key
+ * on the navigation or system bar. On devices with the older hardware button configuration
+ * the recents UI can be accessed with a long press on the Home key.</p>
+ *
+ * <p>When crossing from one task stack to another post-Android 3.0,
+ * the application should synthesize a back stack/history for the new task so that
+ * the user may navigate out of the new task and back to the Launcher by repeated
+ * presses of the back key. Back key presses should not navigate across task stacks.</p>
+ *
+ * <p>TaskStackBuilder provides a way to obey the correct conventions
+ * around cross-task navigation.</p>
+ *
+ * <div class="special reference">
+ * <h3>About Navigation</h3>
+ * For more detailed information about tasks, the back stack, and navigation design guidelines,
+ * please read
+ * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back Stack</a>
+ * from the developer guide and <a href="{@docRoot}design/patterns/navigation.html">Navigation</a>
+ * from the design guide.
+ * </div>
+ */
+public class TaskStackBuilder {
+ private static final String TAG = "TaskStackBuilder";
+
+ private final ArrayList<Intent> mIntents = new ArrayList<Intent>();
+ private final Context mSourceContext;
+
+ private TaskStackBuilder(Context a) {
+ mSourceContext = a;
+ }
+
+ /**
+ * Return a new TaskStackBuilder for launching a fresh task stack consisting
+ * of a series of activities.
+ *
+ * @param context The context that will launch the new task stack or generate a PendingIntent
+ * @return A new TaskStackBuilder
+ */
+ public static TaskStackBuilder create(Context context) {
+ return new TaskStackBuilder(context);
+ }
+
+ /**
+ * Add a new Intent to the task stack. The most recently added Intent will invoke
+ * the Activity at the top of the final task stack.
+ *
+ * @param nextIntent Intent for the next Activity in the synthesized task stack
+ * @return This TaskStackBuilder for method chaining
+ */
+ public TaskStackBuilder addNextIntent(Intent nextIntent) {
+ mIntents.add(nextIntent);
+ return this;
+ }
+
+ /**
+ * Add a new Intent with the resolved chain of parents for the target activity to
+ * the task stack.
+ *
+ * <p>This is equivalent to calling {@link #addParentStack(ComponentName) addParentStack}
+ * with the resolved ComponentName of nextIntent (if it can be resolved), followed by
+ * {@link #addNextIntent(Intent) addNextIntent} with nextIntent.</p>
+ *
+ * @param nextIntent Intent for the topmost Activity in the synthesized task stack.
+ * Its chain of parents as specified in the manifest will be added.
+ * @return This TaskStackBuilder for method chaining.
+ */
+ public TaskStackBuilder addNextIntentWithParentStack(Intent nextIntent) {
+ ComponentName target = nextIntent.getComponent();
+ if (target == null) {
+ target = nextIntent.resolveActivity(mSourceContext.getPackageManager());
+ }
+ if (target != null) {
+ addParentStack(target);
+ }
+ addNextIntent(nextIntent);
+ return this;
+ }
+
+ /**
+ * Add the activity parent chain as specified by the
+ * {@link Activity#getParentActivityIntent() getParentActivityIntent()} method of the activity
+ * specified and the {@link android.R.attr#parentActivityName parentActivityName} attributes
+ * of each successive activity (or activity-alias) element in the application's manifest
+ * to the task stack builder.
+ *
+ * @param sourceActivity All parents of this activity will be added
+ * @return This TaskStackBuilder for method chaining
+ */
+ public TaskStackBuilder addParentStack(Activity sourceActivity) {
+ final Intent parent = sourceActivity.getParentActivityIntent();
+ if (parent != null) {
+ // We have the actual parent intent, build the rest from static metadata
+ // then add the direct parent intent to the end.
+ ComponentName target = parent.getComponent();
+ if (target == null) {
+ target = parent.resolveActivity(mSourceContext.getPackageManager());
+ }
+ addParentStack(target);
+ addNextIntent(parent);
+ }
+ return this;
+ }
+
+ /**
+ * Add the activity parent chain as specified by the
+ * {@link android.R.attr#parentActivityName parentActivityName} attribute of the activity
+ * (or activity-alias) element in the application's manifest to the task stack builder.
+ *
+ * @param sourceActivityClass All parents of this activity will be added
+ * @return This TaskStackBuilder for method chaining
+ */
+ public TaskStackBuilder addParentStack(Class<?> sourceActivityClass) {
+ return addParentStack(new ComponentName(mSourceContext, sourceActivityClass));
+ }
+
+ /**
+ * Add the activity parent chain as specified by the
+ * {@link android.R.attr#parentActivityName parentActivityName} attribute of the activity
+ * (or activity-alias) element in the application's manifest to the task stack builder.
+ *
+ * @param sourceActivityName Must specify an Activity component. All parents of
+ * this activity will be added
+ * @return This TaskStackBuilder for method chaining
+ */
+ public TaskStackBuilder addParentStack(ComponentName sourceActivityName) {
+ final int insertAt = mIntents.size();
+ PackageManager pm = mSourceContext.getPackageManager();
+ try {
+ ActivityInfo info = pm.getActivityInfo(sourceActivityName, 0);
+ String parentActivity = info.parentActivityName;
+ while (parentActivity != null) {
+ final ComponentName target = new ComponentName(info.packageName, parentActivity);
+ info = pm.getActivityInfo(target, 0);
+ parentActivity = info.parentActivityName;
+ final Intent parent = parentActivity == null && insertAt == 0
+ ? Intent.makeMainActivity(target)
+ : new Intent().setComponent(target);
+ mIntents.add(insertAt, parent);
+ }
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Bad ComponentName while traversing activity parent metadata");
+ throw new IllegalArgumentException(e);
+ }
+ return this;
+ }
+
+ /**
+ * @return the number of intents added so far.
+ */
+ public int getIntentCount() {
+ return mIntents.size();
+ }
+
+ /**
+ * Return the intent at the specified index for modification.
+ * Useful if you need to modify the flags or extras of an intent that was previously added,
+ * for example with {@link #addParentStack(Activity)}.
+ *
+ * @param index Index from 0-getIntentCount()
+ * @return the intent at position index
+ */
+ public Intent editIntentAt(int index) {
+ return mIntents.get(index);
+ }
+
+ /**
+ * Start the task stack constructed by this builder.
+ */
+ public void startActivities() {
+ startActivities(null);
+ }
+
+ /**
+ * Start the task stack constructed by this builder.
+ * @hide
+ */
+ public void startActivities(Bundle options, UserHandle userHandle) {
+ if (mIntents.isEmpty()) {
+ throw new IllegalStateException(
+ "No intents added to TaskStackBuilder; cannot startActivities");
+ }
+
+ mSourceContext.startActivitiesAsUser(getIntents(), options, userHandle);
+ }
+
+ /**
+ * Start the task stack constructed by this builder.
+ *
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ */
+ public void startActivities(Bundle options) {
+ startActivities(options, new UserHandle(UserHandle.myUserId()));
+ }
+
+ /**
+ * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far.
+ *
+ * @param requestCode Private request code for the sender
+ * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT},
+ * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT},
+ * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by
+ * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the
+ * intent that can be supplied when the actual send happens.
+ *
+ * @return The obtained PendingIntent
+ */
+ public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags) {
+ return getPendingIntent(requestCode, flags, null);
+ }
+
+ /**
+ * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far.
+ *
+ * @param requestCode Private request code for the sender
+ * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT},
+ * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT},
+ * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by
+ * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the
+ * intent that can be supplied when the actual send happens.
+ * @param options Additional options for how the Activity should be started.
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
+ * Context.startActivity(Intent, Bundle)} for more details.
+ *
+ * @return The obtained PendingIntent
+ */
+ public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags,
+ Bundle options) {
+ if (mIntents.isEmpty()) {
+ throw new IllegalStateException(
+ "No intents added to TaskStackBuilder; cannot getPendingIntent");
+ }
+
+ return PendingIntent.getActivities(mSourceContext, requestCode, getIntents(),
+ flags, options);
+ }
+
+ /**
+ * @hide
+ */
+ public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options,
+ UserHandle user) {
+ if (mIntents.isEmpty()) {
+ throw new IllegalStateException(
+ "No intents added to TaskStackBuilder; cannot getPendingIntent");
+ }
+
+ return PendingIntent.getActivitiesAsUser(mSourceContext, requestCode, getIntents(), flags,
+ options, user);
+ }
+
+ /**
+ * Return an array containing the intents added to this builder. The intent at the
+ * root of the task stack will appear as the first item in the array and the
+ * intent at the top of the stack will appear as the last item.
+ *
+ * @return An array containing the intents added to this builder.
+ */
+ @NonNull
+ public Intent[] getIntents() {
+ Intent[] intents = new Intent[mIntents.size()];
+ if (intents.length == 0) return intents;
+
+ intents[0] = new Intent(mIntents.get(0)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TASK |
+ Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+ for (int i = 1; i < intents.length; i++) {
+ intents[i] = new Intent(mIntents.get(i));
+ }
+ return intents;
+ }
+}
diff --git a/android/app/TaskStackListener.java b/android/app/TaskStackListener.java
new file mode 100644
index 00000000..a52ca0a6
--- /dev/null
+++ b/android/app/TaskStackListener.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.content.ComponentName;
+import android.os.RemoteException;
+
+/**
+ * Classes interested in observing only a subset of changes using ITaskStackListener can extend
+ * this class to avoid having to implement all the methods.
+ * @hide
+ */
+public abstract class TaskStackListener extends ITaskStackListener.Stub {
+ @Override
+ public void onTaskStackChanged() throws RemoteException {
+ }
+
+ @Override
+ public void onActivityPinned(String packageName, int taskId) throws RemoteException {
+ }
+
+ @Override
+ public void onActivityUnpinned() throws RemoteException {
+ }
+
+ @Override
+ public void onPinnedActivityRestartAttempt(boolean clearedTask) throws RemoteException {
+ }
+
+ @Override
+ public void onPinnedStackAnimationStarted() throws RemoteException {
+ }
+
+ @Override
+ public void onPinnedStackAnimationEnded() throws RemoteException {
+ }
+
+ @Override
+ public void onActivityForcedResizable(String packageName, int taskId, int reason)
+ throws RemoteException {
+ }
+
+ @Override
+ public void onActivityDismissingDockedStack() throws RemoteException {
+ }
+
+ @Override
+ public void onActivityLaunchOnSecondaryDisplayFailed() throws RemoteException {
+ }
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException {
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) throws RemoteException {
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) throws RemoteException {
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ }
+
+ @Override
+ public void onTaskDescriptionChanged(int taskId, ActivityManager.TaskDescription td)
+ throws RemoteException {
+ }
+
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation)
+ throws RemoteException {
+ }
+
+ @Override
+ public void onTaskProfileLocked(int taskId, int userId) {
+ }
+
+ @Override
+ public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot)
+ throws RemoteException {
+ }
+}
diff --git a/android/app/TimePickerDialog.java b/android/app/TimePickerDialog.java
new file mode 100644
index 00000000..0f006b66
--- /dev/null
+++ b/android/app/TimePickerDialog.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TimePicker;
+import android.widget.TimePicker.OnTimeChangedListener;
+
+import com.android.internal.R;
+
+/**
+ * A dialog that prompts the user for the time of day using a
+ * {@link TimePicker}.
+ *
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide.
+ */
+public class TimePickerDialog extends AlertDialog implements OnClickListener,
+ OnTimeChangedListener {
+ private static final String HOUR = "hour";
+ private static final String MINUTE = "minute";
+ private static final String IS_24_HOUR = "is24hour";
+
+ private final TimePicker mTimePicker;
+ private final OnTimeSetListener mTimeSetListener;
+
+ private final int mInitialHourOfDay;
+ private final int mInitialMinute;
+ private final boolean mIs24HourView;
+
+ /**
+ * The callback interface used to indicate the user is done filling in
+ * the time (e.g. they clicked on the 'OK' button).
+ */
+ public interface OnTimeSetListener {
+ /**
+ * Called when the user is done setting a new time and the dialog has
+ * closed.
+ *
+ * @param view the view associated with this listener
+ * @param hourOfDay the hour that was set
+ * @param minute the minute that was set
+ */
+ void onTimeSet(TimePicker view, int hourOfDay, int minute);
+ }
+
+ /**
+ * Creates a new time picker dialog.
+ *
+ * @param context the parent context
+ * @param listener the listener to call when the time is set
+ * @param hourOfDay the initial hour
+ * @param minute the initial minute
+ * @param is24HourView whether this is a 24 hour view or AM/PM
+ */
+ public TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute,
+ boolean is24HourView) {
+ this(context, 0, listener, hourOfDay, minute, is24HourView);
+ }
+
+ static int resolveDialogTheme(Context context, int resId) {
+ if (resId == 0) {
+ final TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true);
+ return outValue.resourceId;
+ } else {
+ return resId;
+ }
+ }
+
+ /**
+ * Creates a new time picker dialog with the specified theme.
+ * <p>
+ * The theme is overlaid on top of the theme of the parent {@code context}.
+ * If {@code themeResId} is 0, the dialog will be inflated using the theme
+ * specified by the
+ * {@link android.R.attr#timePickerDialogTheme android:timePickerDialogTheme}
+ * attribute on the parent {@code context}'s theme.
+ *
+ * @param context the parent context
+ * @param themeResId the resource ID of the theme to apply to this dialog
+ * @param listener the listener to call when the time is set
+ * @param hourOfDay the initial hour
+ * @param minute the initial minute
+ * @param is24HourView Whether this is a 24 hour view, or AM/PM.
+ */
+ public TimePickerDialog(Context context, int themeResId, OnTimeSetListener listener,
+ int hourOfDay, int minute, boolean is24HourView) {
+ super(context, resolveDialogTheme(context, themeResId));
+
+ mTimeSetListener = listener;
+ mInitialHourOfDay = hourOfDay;
+ mInitialMinute = minute;
+ mIs24HourView = is24HourView;
+
+ final Context themeContext = getContext();
+ final LayoutInflater inflater = LayoutInflater.from(themeContext);
+ final View view = inflater.inflate(R.layout.time_picker_dialog, null);
+ setView(view);
+ setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this);
+ setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this);
+ setButtonPanelLayoutHint(LAYOUT_HINT_SIDE);
+
+ mTimePicker = (TimePicker) view.findViewById(R.id.timePicker);
+ mTimePicker.setIs24HourView(mIs24HourView);
+ mTimePicker.setCurrentHour(mInitialHourOfDay);
+ mTimePicker.setCurrentMinute(mInitialMinute);
+ mTimePicker.setOnTimeChangedListener(this);
+ }
+
+ /**
+ * @return the time picker displayed in the dialog
+ * @hide For testing only.
+ */
+ @TestApi
+ public TimePicker getTimePicker() {
+ return mTimePicker;
+ }
+
+ @Override
+ public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
+ /* do nothing */
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ getButton(BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mTimePicker.validateInput()) {
+ TimePickerDialog.this.onClick(TimePickerDialog.this, BUTTON_POSITIVE);
+ dismiss();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case BUTTON_POSITIVE:
+ // Note this skips input validation and just uses the last valid time and hour
+ // entry. This will only be invoked programmatically. User clicks on BUTTON_POSITIVE
+ // are handled in show().
+ if (mTimeSetListener != null) {
+ mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
+ mTimePicker.getCurrentMinute());
+ }
+ break;
+ case BUTTON_NEGATIVE:
+ cancel();
+ break;
+ }
+ }
+
+ /**
+ * Sets the current time.
+ *
+ * @param hourOfDay The current hour within the day.
+ * @param minuteOfHour The current minute within the hour.
+ */
+ public void updateTime(int hourOfDay, int minuteOfHour) {
+ mTimePicker.setCurrentHour(hourOfDay);
+ mTimePicker.setCurrentMinute(minuteOfHour);
+ }
+
+ @Override
+ public Bundle onSaveInstanceState() {
+ final Bundle state = super.onSaveInstanceState();
+ state.putInt(HOUR, mTimePicker.getCurrentHour());
+ state.putInt(MINUTE, mTimePicker.getCurrentMinute());
+ state.putBoolean(IS_24_HOUR, mTimePicker.is24HourView());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ final int hour = savedInstanceState.getInt(HOUR);
+ final int minute = savedInstanceState.getInt(MINUTE);
+ mTimePicker.setIs24HourView(savedInstanceState.getBoolean(IS_24_HOUR));
+ mTimePicker.setCurrentHour(hour);
+ mTimePicker.setCurrentMinute(minute);
+ }
+}
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
new file mode 100644
index 00000000..c99de5dd
--- /dev/null
+++ b/android/app/UiAutomation.java
@@ -0,0 +1,1187 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.accessibilityservice.AccessibilityService.Callbacks;
+import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Region;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.WindowAnimationFrameStats;
+import android.view.WindowContentFrameStats;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.accessibility.IAccessibilityInteractionConnection;
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Class for interacting with the device's UI by simulation user actions and
+ * introspection of the screen content. It relies on the platform accessibility
+ * APIs to introspect the screen and to perform some actions on the remote view
+ * tree. It also allows injecting of arbitrary raw input events simulating user
+ * interaction with keyboards and touch devices. One can think of a UiAutomation
+ * as a special type of {@link android.accessibilityservice.AccessibilityService}
+ * which does not provide hooks for the service life cycle and exposes other
+ * APIs that are useful for UI test automation.
+ * <p>
+ * The APIs exposed by this class are low-level to maximize flexibility when
+ * developing UI test automation tools and libraries. Generally, a UiAutomation
+ * client should be using a higher-level library or implement high-level functions.
+ * For example, performing a tap on the screen requires construction and injecting
+ * of a touch down and up events which have to be delivered to the system by a
+ * call to {@link #injectInputEvent(InputEvent, boolean)}.
+ * </p>
+ * <p>
+ * The APIs exposed by this class operate across applications enabling a client
+ * to write tests that cover use cases spanning over multiple applications. For
+ * example, going to the settings application to change a setting and then
+ * interacting with another application whose behavior depends on that setting.
+ * </p>
+ */
+public final class UiAutomation {
+
+ private static final String LOG_TAG = UiAutomation.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ private static final int CONNECTION_ID_UNDEFINED = -1;
+
+ private static final long CONNECT_TIMEOUT_MILLIS = 5000;
+
+ /** Rotation constant: Unfreeze rotation (rotating the device changes its rotation state). */
+ public static final int ROTATION_UNFREEZE = -2;
+
+ /** Rotation constant: Freeze rotation to its current state. */
+ public static final int ROTATION_FREEZE_CURRENT = -1;
+
+ /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */
+ public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0;
+
+ /** Rotation constant: Freeze rotation to 90 degrees . */
+ public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90;
+
+ /** Rotation constant: Freeze rotation to 180 degrees . */
+ public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180;
+
+ /** Rotation constant: Freeze rotation to 270 degrees . */
+ public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
+
+ /**
+ * UiAutomation supresses accessibility services by default. This flag specifies that
+ * existing accessibility services should continue to run, and that new ones may start.
+ * This flag is set when obtaining the UiAutomation from
+ * {@link Instrumentation#getUiAutomation(int)}.
+ */
+ public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 0x00000001;
+
+ private final Object mLock = new Object();
+
+ private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>();
+
+ private final IAccessibilityServiceClient mClient;
+
+ private final IUiAutomationConnection mUiAutomationConnection;
+
+ private int mConnectionId = CONNECTION_ID_UNDEFINED;
+
+ private OnAccessibilityEventListener mOnAccessibilityEventListener;
+
+ private boolean mWaitingForEventDelivery;
+
+ private long mLastEventTimeMillis;
+
+ private boolean mIsConnecting;
+
+ private boolean mIsDestroyed;
+
+ private int mFlags;
+
+ /**
+ * Listener for observing the {@link AccessibilityEvent} stream.
+ */
+ public static interface OnAccessibilityEventListener {
+
+ /**
+ * Callback for receiving an {@link AccessibilityEvent}.
+ * <p>
+ * <strong>Note:</strong> This method is <strong>NOT</strong> executed
+ * on the main test thread. The client is responsible for proper
+ * synchronization.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> It is responsibility of the client
+ * to recycle the received events to minimize object creation.
+ * </p>
+ *
+ * @param event The received event.
+ */
+ public void onAccessibilityEvent(AccessibilityEvent event);
+ }
+
+ /**
+ * Listener for filtering accessibility events.
+ */
+ public static interface AccessibilityEventFilter {
+
+ /**
+ * Callback for determining whether an event is accepted or
+ * it is filtered out.
+ *
+ * @param event The event to process.
+ * @return True if the event is accepted, false to filter it out.
+ */
+ public boolean accept(AccessibilityEvent event);
+ }
+
+ /**
+ * Creates a new instance that will handle callbacks from the accessibility
+ * layer on the thread of the provided looper and perform requests for privileged
+ * operations on the provided connection.
+ *
+ * @param looper The looper on which to execute accessibility callbacks.
+ * @param connection The connection for performing privileged operations.
+ *
+ * @hide
+ */
+ public UiAutomation(Looper looper, IUiAutomationConnection connection) {
+ if (looper == null) {
+ throw new IllegalArgumentException("Looper cannot be null!");
+ }
+ if (connection == null) {
+ throw new IllegalArgumentException("Connection cannot be null!");
+ }
+ mUiAutomationConnection = connection;
+ mClient = new IAccessibilityServiceClientImpl(looper);
+ }
+
+ /**
+ * Connects this UiAutomation to the accessibility introspection APIs with default flags.
+ *
+ * @hide
+ */
+ public void connect() {
+ connect(0);
+ }
+
+ /**
+ * Connects this UiAutomation to the accessibility introspection APIs.
+ *
+ * @param flags Any flags to apply to the automation as it gets connected
+ *
+ * @hide
+ */
+ public void connect(int flags) {
+ synchronized (mLock) {
+ throwIfConnectedLocked();
+ if (mIsConnecting) {
+ return;
+ }
+ mIsConnecting = true;
+ }
+
+ try {
+ // Calling out without a lock held.
+ mUiAutomationConnection.connect(mClient, flags);
+ mFlags = flags;
+ } catch (RemoteException re) {
+ throw new RuntimeException("Error while connecting UiAutomation", re);
+ }
+
+ synchronized (mLock) {
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ try {
+ while (true) {
+ if (isConnectedLocked()) {
+ break;
+ }
+ final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+ final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis;
+ if (remainingTimeMillis <= 0) {
+ throw new RuntimeException("Error while connecting UiAutomation");
+ }
+ try {
+ mLock.wait(remainingTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ } finally {
+ mIsConnecting = false;
+ }
+ }
+ }
+
+ /**
+ * Get the flags used to connect the service.
+ *
+ * @return The flags used to connect
+ *
+ * @hide
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Disconnects this UiAutomation from the accessibility introspection APIs.
+ *
+ * @hide
+ */
+ public void disconnect() {
+ synchronized (mLock) {
+ if (mIsConnecting) {
+ throw new IllegalStateException(
+ "Cannot call disconnect() while connecting!");
+ }
+ throwIfNotConnectedLocked();
+ mConnectionId = CONNECTION_ID_UNDEFINED;
+ }
+ try {
+ // Calling out without a lock held.
+ mUiAutomationConnection.disconnect();
+ } catch (RemoteException re) {
+ throw new RuntimeException("Error while disconnecting UiAutomation", re);
+ }
+ }
+
+ /**
+ * The id of the {@link IAccessibilityInteractionConnection} for querying
+ * the screen content. This is here for legacy purposes since some tools use
+ * hidden APIs to introspect the screen.
+ *
+ * @hide
+ */
+ public int getConnectionId() {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ return mConnectionId;
+ }
+ }
+
+ /**
+ * Reports if the object has been destroyed
+ *
+ * @return {code true} if the object has been destroyed.
+ *
+ * @hide
+ */
+ public boolean isDestroyed() {
+ return mIsDestroyed;
+ }
+
+ /**
+ * Sets a callback for observing the stream of {@link AccessibilityEvent}s.
+ *
+ * @param listener The callback.
+ */
+ public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
+ synchronized (mLock) {
+ mOnAccessibilityEventListener = listener;
+ }
+ }
+
+ /**
+ * Destroy this UiAutomation. After calling this method, attempting to use the object will
+ * result in errors.
+ *
+ * @hide
+ */
+ @TestApi
+ public void destroy() {
+ disconnect();
+ mIsDestroyed = true;
+ }
+
+ /**
+ * Performs a global action. Such an action can be performed at any moment
+ * regardless of the current application or user location in that application.
+ * For example going back, going home, opening recents, etc.
+ *
+ * @param action The action to perform.
+ * @return Whether the action was successfully performed.
+ *
+ * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK
+ * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME
+ * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS
+ * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS
+ */
+ public final boolean performGlobalAction(int action) {
+ final IAccessibilityServiceConnection connection;
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ connection = AccessibilityInteractionClient.getInstance()
+ .getConnection(mConnectionId);
+ }
+ // Calling out without a lock held.
+ if (connection != null) {
+ try {
+ return connection.performGlobalAction(action);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Find the view that has the specified focus type. The search is performed
+ * across all windows.
+ * <p>
+ * <strong>Note:</strong> In order to access the windows you have to opt-in
+ * to retrieve the interactive windows by setting the
+ * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
+ * Otherwise, the search will be performed only in the active window.
+ * </p>
+ *
+ * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or
+ * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
+ * @return The node info of the focused view or null.
+ *
+ * @see AccessibilityNodeInfo#FOCUS_INPUT
+ * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
+ */
+ public AccessibilityNodeInfo findFocus(int focus) {
+ return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId,
+ AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus);
+ }
+
+ /**
+ * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation.
+ * This method is useful if one wants to change some of the dynamically
+ * configurable properties at runtime.
+ *
+ * @return The accessibility service info.
+ *
+ * @see AccessibilityServiceInfo
+ */
+ public final AccessibilityServiceInfo getServiceInfo() {
+ final IAccessibilityServiceConnection connection;
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ connection = AccessibilityInteractionClient.getInstance()
+ .getConnection(mConnectionId);
+ }
+ // Calling out without a lock held.
+ if (connection != null) {
+ try {
+ return connection.getServiceInfo();
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the {@link AccessibilityServiceInfo} that describes how this
+ * UiAutomation will be handled by the platform accessibility layer.
+ *
+ * @param info The info.
+ *
+ * @see AccessibilityServiceInfo
+ */
+ public final void setServiceInfo(AccessibilityServiceInfo info) {
+ final IAccessibilityServiceConnection connection;
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ AccessibilityInteractionClient.getInstance().clearCache();
+ connection = AccessibilityInteractionClient.getInstance()
+ .getConnection(mConnectionId);
+ }
+ // Calling out without a lock held.
+ if (connection != null) {
+ try {
+ connection.setServiceInfo(info);
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
+ }
+ }
+ }
+
+ /**
+ * Gets the windows on the screen. This method returns only the windows
+ * that a sighted user can interact with, as opposed to all windows.
+ * For example, if there is a modal dialog shown and the user cannot touch
+ * anything behind it, then only the modal window will be reported
+ * (assuming it is the top one). For convenience the returned windows
+ * are ordered in a descending layer order, which is the windows that
+ * are higher in the Z-order are reported first.
+ * <p>
+ * <strong>Note:</strong> In order to access the windows you have to opt-in
+ * to retrieve the interactive windows by setting the
+ * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
+ * </p>
+ *
+ * @return The windows if there are windows such, otherwise an empty list.
+ */
+ public List<AccessibilityWindowInfo> getWindows() {
+ final int connectionId;
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ connectionId = mConnectionId;
+ }
+ // Calling out without a lock held.
+ return AccessibilityInteractionClient.getInstance()
+ .getWindows(connectionId);
+ }
+
+ /**
+ * Gets the root {@link AccessibilityNodeInfo} in the active window.
+ *
+ * @return The root info.
+ */
+ public AccessibilityNodeInfo getRootInActiveWindow() {
+ final int connectionId;
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ connectionId = mConnectionId;
+ }
+ // Calling out without a lock held.
+ return AccessibilityInteractionClient.getInstance()
+ .getRootInActiveWindow(connectionId);
+ }
+
+ /**
+ * A method for injecting an arbitrary input event.
+ * <p>
+ * <strong>Note:</strong> It is caller's responsibility to recycle the event.
+ * </p>
+ * @param event The event to inject.
+ * @param sync Whether to inject the event synchronously.
+ * @return Whether event injection succeeded.
+ */
+ public boolean injectInputEvent(InputEvent event, boolean sync) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync);
+ }
+ // Calling out without a lock held.
+ return mUiAutomationConnection.injectInputEvent(event, sync);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while injecting input event!", re);
+ }
+ return false;
+ }
+
+ /**
+ * Sets the device rotation. A client can freeze the rotation in
+ * desired state or freeze the rotation to its current state or
+ * unfreeze the rotation (rotating the device changes its rotation
+ * state).
+ *
+ * @param rotation The desired rotation.
+ * @return Whether the rotation was set successfully.
+ *
+ * @see #ROTATION_FREEZE_0
+ * @see #ROTATION_FREEZE_90
+ * @see #ROTATION_FREEZE_180
+ * @see #ROTATION_FREEZE_270
+ * @see #ROTATION_FREEZE_CURRENT
+ * @see #ROTATION_UNFREEZE
+ */
+ public boolean setRotation(int rotation) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ switch (rotation) {
+ case ROTATION_FREEZE_0:
+ case ROTATION_FREEZE_90:
+ case ROTATION_FREEZE_180:
+ case ROTATION_FREEZE_270:
+ case ROTATION_UNFREEZE:
+ case ROTATION_FREEZE_CURRENT: {
+ try {
+ // Calling out without a lock held.
+ mUiAutomationConnection.setRotation(rotation);
+ return true;
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while setting rotation!", re);
+ }
+ } return false;
+ default: {
+ throw new IllegalArgumentException("Invalid rotation.");
+ }
+ }
+ }
+
+ /**
+ * Executes a command and waits for a specific accessibility event up to a
+ * given wait timeout. To detect a sequence of events one can implement a
+ * filter that keeps track of seen events of the expected sequence and
+ * returns true after the last event of that sequence is received.
+ * <p>
+ * <strong>Note:</strong> It is caller's responsibility to recycle the returned event.
+ * </p>
+ * @param command The command to execute.
+ * @param filter Filter that recognizes the expected event.
+ * @param timeoutMillis The wait timeout in milliseconds.
+ *
+ * @throws TimeoutException If the expected event is not received within the timeout.
+ */
+ public AccessibilityEvent executeAndWaitForEvent(Runnable command,
+ AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
+ // Acquire the lock and prepare for receiving events.
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ mEventQueue.clear();
+ // Prepare to wait for an event.
+ mWaitingForEventDelivery = true;
+ }
+
+ // Note: We have to release the lock since calling out with this lock held
+ // can bite. We will correctly filter out events from other interactions,
+ // so starting to collect events before running the action is just fine.
+
+ // We will ignore events from previous interactions.
+ final long executionStartTimeMillis = SystemClock.uptimeMillis();
+ // Execute the command *without* the lock being held.
+ command.run();
+
+ // Acquire the lock and wait for the event.
+ try {
+ // Wait for the event.
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ while (true) {
+ List<AccessibilityEvent> localEvents = new ArrayList<>();
+ synchronized (mLock) {
+ localEvents.addAll(mEventQueue);
+ mEventQueue.clear();
+ }
+ // Drain the event queue
+ while (!localEvents.isEmpty()) {
+ AccessibilityEvent event = localEvents.remove(0);
+ // Ignore events from previous interactions.
+ if (event.getEventTime() < executionStartTimeMillis) {
+ continue;
+ }
+ if (filter.accept(event)) {
+ return event;
+ }
+ event.recycle();
+ }
+ // Check if timed out and if not wait.
+ final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+ final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
+ if (remainingTimeMillis <= 0) {
+ throw new TimeoutException("Expected event not received within: "
+ + timeoutMillis + " ms.");
+ }
+ synchronized (mLock) {
+ if (mEventQueue.isEmpty()) {
+ try {
+ mLock.wait(remainingTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+ }
+ } finally {
+ synchronized (mLock) {
+ mWaitingForEventDelivery = false;
+ mEventQueue.clear();
+ mLock.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Waits for the accessibility event stream to become idle, which is not to
+ * have received an accessibility event within <code>idleTimeoutMillis</code>.
+ * The total time spent to wait for an idle accessibility event stream is bounded
+ * by the <code>globalTimeoutMillis</code>.
+ *
+ * @param idleTimeoutMillis The timeout in milliseconds between two events
+ * to consider the device idle.
+ * @param globalTimeoutMillis The maximal global timeout in milliseconds in
+ * which to wait for an idle state.
+ *
+ * @throws TimeoutException If no idle state was detected within
+ * <code>globalTimeoutMillis.</code>
+ */
+ public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
+ throws TimeoutException {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ if (mLastEventTimeMillis <= 0) {
+ mLastEventTimeMillis = startTimeMillis;
+ }
+
+ while (true) {
+ final long currentTimeMillis = SystemClock.uptimeMillis();
+ // Did we get idle state within the global timeout?
+ final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
+ final long remainingGlobalTimeMillis =
+ globalTimeoutMillis - elapsedGlobalTimeMillis;
+ if (remainingGlobalTimeMillis <= 0) {
+ throw new TimeoutException("No idle state with idle timeout: "
+ + idleTimeoutMillis + " within global timeout: "
+ + globalTimeoutMillis);
+ }
+ // Did we get an idle state within the idle timeout?
+ final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
+ final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
+ if (remainingIdleTimeMillis <= 0) {
+ return;
+ }
+ try {
+ mLock.wait(remainingIdleTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+ }
+
+ /**
+ * Takes a screenshot.
+ *
+ * @return The screenshot bitmap on success, null otherwise.
+ */
+ public Bitmap takeScreenshot() {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ Display display = DisplayManagerGlobal.getInstance()
+ .getRealDisplay(Display.DEFAULT_DISPLAY);
+ Point displaySize = new Point();
+ display.getRealSize(displaySize);
+ final int displayWidth = displaySize.x;
+ final int displayHeight = displaySize.y;
+
+ final float screenshotWidth;
+ final float screenshotHeight;
+
+ final int rotation = display.getRotation();
+ switch (rotation) {
+ case ROTATION_FREEZE_0: {
+ screenshotWidth = displayWidth;
+ screenshotHeight = displayHeight;
+ } break;
+ case ROTATION_FREEZE_90: {
+ screenshotWidth = displayHeight;
+ screenshotHeight = displayWidth;
+ } break;
+ case ROTATION_FREEZE_180: {
+ screenshotWidth = displayWidth;
+ screenshotHeight = displayHeight;
+ } break;
+ case ROTATION_FREEZE_270: {
+ screenshotWidth = displayHeight;
+ screenshotHeight = displayWidth;
+ } break;
+ default: {
+ throw new IllegalArgumentException("Invalid rotation: "
+ + rotation);
+ }
+ }
+
+ // Take the screenshot
+ Bitmap screenShot = null;
+ try {
+ // Calling out without a lock held.
+ screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth,
+ (int) screenshotHeight);
+ if (screenShot == null) {
+ return null;
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while taking screnshot!", re);
+ return null;
+ }
+
+ // Rotate the screenshot to the current orientation
+ if (rotation != ROTATION_FREEZE_0) {
+ Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight,
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(unrotatedScreenShot);
+ canvas.translate(unrotatedScreenShot.getWidth() / 2,
+ unrotatedScreenShot.getHeight() / 2);
+ canvas.rotate(getDegreesForRotation(rotation));
+ canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2);
+ canvas.drawBitmap(screenShot, 0, 0, null);
+ canvas.setBitmap(null);
+ screenShot.recycle();
+ screenShot = unrotatedScreenShot;
+ }
+
+ // Optimization
+ screenShot.setHasAlpha(false);
+
+ return screenShot;
+ }
+
+ /**
+ * Sets whether this UiAutomation to run in a "monkey" mode. Applications can query whether
+ * they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing
+ * potentially undesirable actions such as calling 911 or posting on public forums etc.
+ *
+ * @param enable whether to run in a "monkey" mode or not. Default is not.
+ * @see ActivityManager#isUserAMonkey()
+ */
+ public void setRunAsMonkey(boolean enable) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ ActivityManager.getService().setUserIsMonkey(enable);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while setting run as monkey!", re);
+ }
+ }
+
+ /**
+ * Clears the frame statistics for the content of a given window. These
+ * statistics contain information about the most recently rendered content
+ * frames.
+ *
+ * @param windowId The window id.
+ * @return Whether the window is present and its frame statistics
+ * were cleared.
+ *
+ * @see android.view.WindowContentFrameStats
+ * @see #getWindowContentFrameStats(int)
+ * @see #getWindows()
+ * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
+ */
+ public boolean clearWindowContentFrameStats(int windowId) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId);
+ }
+ // Calling out without a lock held.
+ return mUiAutomationConnection.clearWindowContentFrameStats(windowId);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error clearing window content frame stats!", re);
+ }
+ return false;
+ }
+
+ /**
+ * Gets the frame statistics for a given window. These statistics contain
+ * information about the most recently rendered content frames.
+ * <p>
+ * A typical usage requires clearing the window frame statistics via {@link
+ * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and
+ * finally getting the window frame statistics via calling this method.
+ * </p>
+ * <pre>
+ * // Assume we have at least one window.
+ * final int windowId = getWindows().get(0).getId();
+ *
+ * // Start with a clean slate.
+ * uiAutimation.clearWindowContentFrameStats(windowId);
+ *
+ * // Do stuff with the UI.
+ *
+ * // Get the frame statistics.
+ * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId);
+ * </pre>
+ *
+ * @param windowId The window id.
+ * @return The window frame statistics, or null if the window is not present.
+ *
+ * @see android.view.WindowContentFrameStats
+ * @see #clearWindowContentFrameStats(int)
+ * @see #getWindows()
+ * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
+ */
+ public WindowContentFrameStats getWindowContentFrameStats(int windowId) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId);
+ }
+ // Calling out without a lock held.
+ return mUiAutomationConnection.getWindowContentFrameStats(windowId);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error getting window content frame stats!", re);
+ }
+ return null;
+ }
+
+ /**
+ * Clears the window animation rendering statistics. These statistics contain
+ * information about the most recently rendered window animation frames, i.e.
+ * for window transition animations.
+ *
+ * @see android.view.WindowAnimationFrameStats
+ * @see #getWindowAnimationFrameStats()
+ * @see android.R.styleable#WindowAnimation
+ */
+ public void clearWindowAnimationFrameStats() {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Clearing window animation frame stats");
+ }
+ // Calling out without a lock held.
+ mUiAutomationConnection.clearWindowAnimationFrameStats();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error clearing window animation frame stats!", re);
+ }
+ }
+
+ /**
+ * Gets the window animation frame statistics. These statistics contain
+ * information about the most recently rendered window animation frames, i.e.
+ * for window transition animations.
+ *
+ * <p>
+ * A typical usage requires clearing the window animation frame statistics via
+ * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes
+ * a window transition which uses a window animation and finally getting the window
+ * animation frame statistics by calling this method.
+ * </p>
+ * <pre>
+ * // Start with a clean slate.
+ * uiAutimation.clearWindowAnimationFrameStats();
+ *
+ * // Do stuff to trigger a window transition.
+ *
+ * // Get the frame statistics.
+ * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats();
+ * </pre>
+ *
+ * @return The window animation frame statistics.
+ *
+ * @see android.view.WindowAnimationFrameStats
+ * @see #clearWindowAnimationFrameStats()
+ * @see android.R.styleable#WindowAnimation
+ */
+ public WindowAnimationFrameStats getWindowAnimationFrameStats() {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Getting window animation frame stats");
+ }
+ // Calling out without a lock held.
+ return mUiAutomationConnection.getWindowAnimationFrameStats();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error getting window animation frame stats!", re);
+ }
+ return null;
+ }
+
+ /**
+ * Grants a runtime permission to a package for a user.
+ * @param packageName The package to which to grant.
+ * @param permission The permission to grant.
+ * @return Whether granting succeeded.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean grantRuntimePermission(String packageName, String permission,
+ UserHandle userHandle) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Granting runtime permission");
+ }
+ // Calling out without a lock held.
+ mUiAutomationConnection.grantRuntimePermission(packageName,
+ permission, userHandle.getIdentifier());
+ // TODO: The package manager API should return boolean.
+ return true;
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error granting runtime permission", re);
+ }
+ return false;
+ }
+
+ /**
+ * Revokes a runtime permission from a package for a user.
+ * @param packageName The package from which to revoke.
+ * @param permission The permission to revoke.
+ * @return Whether revoking succeeded.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean revokeRuntimePermission(String packageName, String permission,
+ UserHandle userHandle) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+ try {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Revoking runtime permission");
+ }
+ // Calling out without a lock held.
+ mUiAutomationConnection.revokeRuntimePermission(packageName,
+ permission, userHandle.getIdentifier());
+ // TODO: The package manager API should return boolean.
+ return true;
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error revoking runtime permission", re);
+ }
+ return false;
+ }
+
+ /**
+ * Executes a shell command. This method returns a file descriptor that points
+ * to the standard output stream. The command execution is similar to running
+ * "adb shell <command>" from a host connected to the device.
+ * <p>
+ * <strong>Note:</strong> It is your responsibility to close the returned file
+ * descriptor once you are done reading.
+ * </p>
+ *
+ * @param command The command to execute.
+ * @return A file descriptor to the standard output stream.
+ */
+ public ParcelFileDescriptor executeShellCommand(String command) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+
+ ParcelFileDescriptor source = null;
+ ParcelFileDescriptor sink = null;
+
+ try {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ source = pipe[0];
+ sink = pipe[1];
+
+ // Calling out without a lock held.
+ mUiAutomationConnection.executeShellCommand(command, sink, null);
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "Error executing shell command!", ioe);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error executing shell command!", re);
+ } finally {
+ IoUtils.closeQuietly(sink);
+ }
+
+ return source;
+ }
+
+ /**
+ * Executes a shell command. This method returns two file descriptors,
+ * one that points to the standard output stream (element at index 0), and one that points
+ * to the standard input stream (element at index 1). The command execution is similar
+ * to running "adb shell <command>" from a host connected to the device.
+ * <p>
+ * <strong>Note:</strong> It is your responsibility to close the returned file
+ * descriptors once you are done reading/writing.
+ * </p>
+ *
+ * @param command The command to execute.
+ * @return File descriptors (out, in) to the standard output/input streams.
+ *
+ * @hide
+ */
+ @TestApi
+ public ParcelFileDescriptor[] executeShellCommandRw(String command) {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ }
+
+ ParcelFileDescriptor source_read = null;
+ ParcelFileDescriptor sink_read = null;
+
+ ParcelFileDescriptor source_write = null;
+ ParcelFileDescriptor sink_write = null;
+
+ try {
+ ParcelFileDescriptor[] pipe_read = ParcelFileDescriptor.createPipe();
+ source_read = pipe_read[0];
+ sink_read = pipe_read[1];
+
+ ParcelFileDescriptor[] pipe_write = ParcelFileDescriptor.createPipe();
+ source_write = pipe_write[0];
+ sink_write = pipe_write[1];
+
+ // Calling out without a lock held.
+ mUiAutomationConnection.executeShellCommand(command, sink_read, source_write);
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "Error executing shell command!", ioe);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error executing shell command!", re);
+ } finally {
+ IoUtils.closeQuietly(sink_read);
+ IoUtils.closeQuietly(source_write);
+ }
+
+ ParcelFileDescriptor[] result = new ParcelFileDescriptor[2];
+ result[0] = source_read;
+ result[1] = sink_write;
+ return result;
+ }
+
+ private static float getDegreesForRotation(int value) {
+ switch (value) {
+ case Surface.ROTATION_90: {
+ return 360f - 90f;
+ }
+ case Surface.ROTATION_180: {
+ return 360f - 180f;
+ }
+ case Surface.ROTATION_270: {
+ return 360f - 270f;
+ } default: {
+ return 0;
+ }
+ }
+ }
+
+ private boolean isConnectedLocked() {
+ return mConnectionId != CONNECTION_ID_UNDEFINED;
+ }
+
+ private void throwIfConnectedLocked() {
+ if (mConnectionId != CONNECTION_ID_UNDEFINED) {
+ throw new IllegalStateException("UiAutomation not connected!");
+ }
+ }
+
+ private void throwIfNotConnectedLocked() {
+ if (!isConnectedLocked()) {
+ throw new IllegalStateException("UiAutomation not connected!");
+ }
+ }
+
+ private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {
+
+ public IAccessibilityServiceClientImpl(Looper looper) {
+ super(null, looper, new Callbacks() {
+ @Override
+ public void init(int connectionId, IBinder windowToken) {
+ synchronized (mLock) {
+ mConnectionId = connectionId;
+ mLock.notifyAll();
+ }
+ }
+
+ @Override
+ public void onServiceConnected() {
+ /* do nothing */
+ }
+
+ @Override
+ public void onInterrupt() {
+ /* do nothing */
+ }
+
+ @Override
+ public boolean onGesture(int gestureId) {
+ /* do nothing */
+ return false;
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ mLastEventTimeMillis = event.getEventTime();
+ if (mWaitingForEventDelivery) {
+ mEventQueue.add(AccessibilityEvent.obtain(event));
+ }
+ mLock.notifyAll();
+ }
+ // Calling out only without a lock held.
+ final OnAccessibilityEventListener listener = mOnAccessibilityEventListener;
+ if (listener != null) {
+ listener.onAccessibilityEvent(AccessibilityEvent.obtain(event));
+ }
+ }
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onMagnificationChanged(@NonNull Region region,
+ float scale, float centerX, float centerY) {
+ /* do nothing */
+ }
+
+ @Override
+ public void onSoftKeyboardShowModeChanged(int showMode) {
+ /* do nothing */
+ }
+
+ @Override
+ public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
+ /* do nothing */
+ }
+
+ @Override
+ public void onFingerprintCapturingGesturesChanged(boolean active) {
+ /* do nothing */
+ }
+
+ @Override
+ public void onFingerprintGesture(int gesture) {
+ /* do nothing */
+ }
+
+ @Override
+ public void onAccessibilityButtonClicked() {
+ /* do nothing */
+ }
+
+ @Override
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ /* do nothing */
+ }
+ });
+ }
+ }
+}
diff --git a/android/app/UiAutomationConnection.java b/android/app/UiAutomationConnection.java
new file mode 100644
index 00000000..5e414b83
--- /dev/null
+++ b/android/app/UiAutomationConnection.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.graphics.Bitmap;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.view.IWindowManager;
+import android.view.InputEvent;
+import android.view.SurfaceControl;
+import android.view.WindowAnimationFrameStats;
+import android.view.WindowContentFrameStats;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.IAccessibilityManager;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * This is a remote object that is passed from the shell to an instrumentation
+ * for enabling access to privileged operations which the shell can do and the
+ * instrumentation cannot. These privileged operations are needed for implementing
+ * a {@link UiAutomation} that enables across application testing by simulating
+ * user actions and performing screen introspection.
+ *
+ * @hide
+ */
+public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
+
+ private static final String TAG = "UiAutomationConnection";
+
+ private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1;
+
+ private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface(
+ ServiceManager.getService(Service.WINDOW_SERVICE));
+
+ private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager.Stub
+ .asInterface(ServiceManager.getService(Service.ACCESSIBILITY_SERVICE));
+
+ private final IPackageManager mPackageManager = IPackageManager.Stub
+ .asInterface(ServiceManager.getService("package"));
+
+ private final Object mLock = new Object();
+
+ private final Binder mToken = new Binder();
+
+ private int mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED;
+
+ private IAccessibilityServiceClient mClient;
+
+ private boolean mIsShutdown;
+
+ private int mOwningUid;
+
+ @Override
+ public void connect(IAccessibilityServiceClient client, int flags) {
+ if (client == null) {
+ throw new IllegalArgumentException("Client cannot be null!");
+ }
+ synchronized (mLock) {
+ throwIfShutdownLocked();
+ if (isConnectedLocked()) {
+ throw new IllegalStateException("Already connected.");
+ }
+ mOwningUid = Binder.getCallingUid();
+ registerUiTestAutomationServiceLocked(client, flags);
+ storeRotationStateLocked();
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ if (!isConnectedLocked()) {
+ throw new IllegalStateException("Already disconnected.");
+ }
+ mOwningUid = -1;
+ unregisterUiTestAutomationServiceLocked();
+ restoreRotationStateLocked();
+ }
+ }
+
+ @Override
+ public boolean injectInputEvent(InputEvent event, boolean sync) {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
+ : InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return InputManager.getInstance().injectInputEvent(event, mode);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public boolean setRotation(int rotation) {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (rotation == UiAutomation.ROTATION_UNFREEZE) {
+ mWindowManager.thawRotation();
+ } else {
+ mWindowManager.freezeRotation(rotation);
+ }
+ return true;
+ } catch (RemoteException re) {
+ /* ignore */
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return false;
+ }
+
+ @Override
+ public Bitmap takeScreenshot(int width, int height) {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return SurfaceControl.screenshot(width, height);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public boolean clearWindowContentFrameStats(int windowId) throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ int callingUserId = UserHandle.getCallingUserId();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
+ if (token == null) {
+ return false;
+ }
+ return mWindowManager.clearWindowContentFrameStats(token);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public WindowContentFrameStats getWindowContentFrameStats(int windowId) throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ int callingUserId = UserHandle.getCallingUserId();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
+ if (token == null) {
+ return null;
+ }
+ return mWindowManager.getWindowContentFrameStats(token);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void clearWindowAnimationFrameStats() {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ SurfaceControl.clearAnimationFrameStats();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public WindowAnimationFrameStats getWindowAnimationFrameStats() {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ WindowAnimationFrameStats stats = new WindowAnimationFrameStats();
+ SurfaceControl.getAnimationFrameStats(stats);
+ return stats;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void grantRuntimePermission(String packageName, String permission, int userId)
+ throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mPackageManager.grantRuntimePermission(packageName, permission, userId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void revokeRuntimePermission(String packageName, String permission, int userId)
+ throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mPackageManager.revokeRuntimePermission(packageName, permission, userId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ public class Repeater implements Runnable {
+ // Continuously read readFrom and write back to writeTo until EOF is encountered
+ private final InputStream readFrom;
+ private final OutputStream writeTo;
+ public Repeater (InputStream readFrom, OutputStream writeTo) {
+ this.readFrom = readFrom;
+ this.writeTo = writeTo;
+ }
+ @Override
+ public void run() {
+ try {
+ final byte[] buffer = new byte[8192];
+ int readByteCount;
+ while (true) {
+ readByteCount = readFrom.read(buffer);
+ if (readByteCount < 0) {
+ break;
+ }
+ writeTo.write(buffer, 0, readByteCount);
+ writeTo.flush();
+ }
+ } catch (IOException ioe) {
+ throw new RuntimeException("Error while reading/writing ", ioe);
+ } finally {
+ IoUtils.closeQuietly(readFrom);
+ IoUtils.closeQuietly(writeTo);
+ }
+ }
+ }
+
+ @Override
+ public void executeShellCommand(final String command, final ParcelFileDescriptor sink,
+ final ParcelFileDescriptor source) throws RemoteException {
+ synchronized (mLock) {
+ throwIfCalledByNotTrustedUidLocked();
+ throwIfShutdownLocked();
+ throwIfNotConnectedLocked();
+ }
+ final java.lang.Process process;
+
+ try {
+ process = Runtime.getRuntime().exec(command);
+ } catch (IOException exc) {
+ throw new RuntimeException("Error running shell command '" + command + "'", exc);
+ }
+
+ // Read from process and write to pipe
+ final Thread readFromProcess;
+ if (sink != null) {
+ InputStream sink_in = process.getInputStream();;
+ OutputStream sink_out = new FileOutputStream(sink.getFileDescriptor());
+
+ readFromProcess = new Thread(new Repeater(sink_in, sink_out));
+ readFromProcess.start();
+ } else {
+ readFromProcess = null;
+ }
+
+ // Read from pipe and write to process
+ final Thread writeToProcess;
+ if (source != null) {
+ OutputStream source_out = process.getOutputStream();
+ InputStream source_in = new FileInputStream(source.getFileDescriptor());
+
+ writeToProcess = new Thread(new Repeater(source_in, source_out));
+ writeToProcess.start();
+ } else {
+ writeToProcess = null;
+ }
+
+ Thread cleanup = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (writeToProcess != null) {
+ writeToProcess.join();
+ }
+ if (readFromProcess != null) {
+ readFromProcess.join();
+ }
+ } catch (InterruptedException exc) {
+ Log.e(TAG, "At least one of the threads was interrupted");
+ }
+ IoUtils.closeQuietly(sink);
+ IoUtils.closeQuietly(source);
+ process.destroy();
+ }
+ });
+ cleanup.start();
+ }
+
+ @Override
+ public void shutdown() {
+ synchronized (mLock) {
+ if (isConnectedLocked()) {
+ throwIfCalledByNotTrustedUidLocked();
+ }
+ throwIfShutdownLocked();
+ mIsShutdown = true;
+ if (isConnectedLocked()) {
+ disconnect();
+ }
+ }
+ }
+
+ private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
+ int flags) {
+ IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
+ ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
+ final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
+ info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
+ info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
+ | AccessibilityServiceInfo.FLAG_FORCE_DIRECT_BOOT_AWARE;
+ info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
+ | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
+ | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY
+ | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS);
+ try {
+ // Calling out with a lock held is fine since if the system
+ // process is gone the client calling in will be killed.
+ manager.registerUiTestAutomationService(mToken, client, info, flags);
+ mClient = client;
+ } catch (RemoteException re) {
+ throw new IllegalStateException("Error while registering UiTestAutomationService.", re);
+ }
+ }
+
+ private void unregisterUiTestAutomationServiceLocked() {
+ IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
+ ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
+ try {
+ // Calling out with a lock held is fine since if the system
+ // process is gone the client calling in will be killed.
+ manager.unregisterUiTestAutomationService(mClient);
+ mClient = null;
+ } catch (RemoteException re) {
+ throw new IllegalStateException("Error while unregistering UiTestAutomationService",
+ re);
+ }
+ }
+
+ private void storeRotationStateLocked() {
+ try {
+ if (mWindowManager.isRotationFrozen()) {
+ // Calling out with a lock held is fine since if the system
+ // process is gone the client calling in will be killed.
+ mInitialFrozenRotation = mWindowManager.getDefaultDisplayRotation();
+ }
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+
+ private void restoreRotationStateLocked() {
+ try {
+ if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) {
+ // Calling out with a lock held is fine since if the system
+ // process is gone the client calling in will be killed.
+ mWindowManager.freezeRotation(mInitialFrozenRotation);
+ } else {
+ // Calling out with a lock held is fine since if the system
+ // process is gone the client calling in will be killed.
+ mWindowManager.thawRotation();
+ }
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+
+ private boolean isConnectedLocked() {
+ return mClient != null;
+ }
+
+ private void throwIfShutdownLocked() {
+ if (mIsShutdown) {
+ throw new IllegalStateException("Connection shutdown!");
+ }
+ }
+
+ private void throwIfNotConnectedLocked() {
+ if (!isConnectedLocked()) {
+ throw new IllegalStateException("Not connected!");
+ }
+ }
+
+ private void throwIfCalledByNotTrustedUidLocked() {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != mOwningUid && mOwningUid != Process.SYSTEM_UID
+ && callingUid != 0 /*root*/) {
+ throw new SecurityException("Calling from not trusted UID!");
+ }
+ }
+}
diff --git a/android/app/UiModeManager.java b/android/app/UiModeManager.java
new file mode 100644
index 00000000..bc616686
--- /dev/null
+++ b/android/app/UiModeManager.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class provides access to the system uimode services. These services
+ * allow applications to control UI modes of the device.
+ * It provides functionality to disable the car mode and it gives access to the
+ * night mode settings.
+ *
+ * <p>These facilities are built on top of the underlying
+ * {@link android.content.Intent#ACTION_DOCK_EVENT} broadcasts that are sent when the user
+ * physical places the device into and out of a dock. When that happens,
+ * the UiModeManager switches the system {@link android.content.res.Configuration}
+ * to the appropriate UI mode, sends broadcasts about the mode switch, and
+ * starts the corresponding mode activity if appropriate. See the
+ * broadcasts {@link #ACTION_ENTER_CAR_MODE} and
+ * {@link #ACTION_ENTER_DESK_MODE} for more information.
+ *
+ * <p>In addition, the user may manually switch the system to car mode without
+ * physically being in a dock. While in car mode -- whether by manual action
+ * from the user or being physically placed in a dock -- a notification is
+ * displayed allowing the user to exit dock mode. Thus the dock mode
+ * represented here may be different than the current state of the underlying
+ * dock event broadcast.
+ */
+@SystemService(Context.UI_MODE_SERVICE)
+public class UiModeManager {
+ private static final String TAG = "UiModeManager";
+
+ /**
+ * Broadcast sent when the device's UI has switched to car mode, either
+ * by being placed in a car dock or explicit action of the user. After
+ * sending the broadcast, the system will start the intent
+ * {@link android.content.Intent#ACTION_MAIN} with category
+ * {@link android.content.Intent#CATEGORY_CAR_DOCK}
+ * to display the car UI, which typically what an application would
+ * implement to provide their own interface. However, applications can
+ * also monitor this Intent in order to be informed of mode changes or
+ * prevent the normal car UI from being displayed by setting the result
+ * of the broadcast to {@link Activity#RESULT_CANCELED}.
+ */
+ public static String ACTION_ENTER_CAR_MODE = "android.app.action.ENTER_CAR_MODE";
+
+ /**
+ * Broadcast sent when the device's UI has switch away from car mode back
+ * to normal mode. Typically used by a car mode app, to dismiss itself
+ * when the user exits car mode.
+ */
+ public static String ACTION_EXIT_CAR_MODE = "android.app.action.EXIT_CAR_MODE";
+
+ /**
+ * Broadcast sent when the device's UI has switched to desk mode,
+ * by being placed in a desk dock. After
+ * sending the broadcast, the system will start the intent
+ * {@link android.content.Intent#ACTION_MAIN} with category
+ * {@link android.content.Intent#CATEGORY_DESK_DOCK}
+ * to display the desk UI, which typically what an application would
+ * implement to provide their own interface. However, applications can
+ * also monitor this Intent in order to be informed of mode changes or
+ * prevent the normal desk UI from being displayed by setting the result
+ * of the broadcast to {@link Activity#RESULT_CANCELED}.
+ */
+ public static String ACTION_ENTER_DESK_MODE = "android.app.action.ENTER_DESK_MODE";
+
+ /**
+ * Broadcast sent when the device's UI has switched away from desk mode back
+ * to normal mode. Typically used by a desk mode app, to dismiss itself
+ * when the user exits desk mode.
+ */
+ public static String ACTION_EXIT_DESK_MODE = "android.app.action.EXIT_DESK_MODE";
+
+ /** @hide */
+ @IntDef({MODE_NIGHT_AUTO, MODE_NIGHT_NO, MODE_NIGHT_YES})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NightMode {}
+
+ /**
+ * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}:
+ * automatically switch night mode on and off based on the time.
+ */
+ public static final int MODE_NIGHT_AUTO = Configuration.UI_MODE_NIGHT_UNDEFINED >> 4;
+
+ /**
+ * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}:
+ * never run in night mode.
+ */
+ public static final int MODE_NIGHT_NO = Configuration.UI_MODE_NIGHT_NO >> 4;
+
+ /**
+ * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}:
+ * always run in night mode.
+ */
+ public static final int MODE_NIGHT_YES = Configuration.UI_MODE_NIGHT_YES >> 4;
+
+ private IUiModeManager mService;
+
+ /*package*/ UiModeManager() throws ServiceNotFoundException {
+ mService = IUiModeManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.UI_MODE_SERVICE));
+ }
+
+ /**
+ * Flag for use with {@link #enableCarMode(int)}: go to the car
+ * home activity as part of the enable. Enabling this way ensures
+ * a clean transition between the current activity (in non-car-mode) and
+ * the car home activity that will serve as home while in car mode. This
+ * will switch to the car home activity even if we are already in car mode.
+ */
+ public static final int ENABLE_CAR_MODE_GO_CAR_HOME = 0x0001;
+
+ /**
+ * Flag for use with {@link #enableCarMode(int)}: allow sleep mode while in car mode.
+ * By default, when this flag is not set, the system may hold a full wake lock to keep the
+ * screen turned on and prevent the system from entering sleep mode while in car mode.
+ * Setting this flag disables such behavior and the system may enter sleep mode
+ * if there is no other user activity and no other wake lock held.
+ * Setting this flag can be relevant for a car dock application that does not require the
+ * screen kept on.
+ */
+ public static final int ENABLE_CAR_MODE_ALLOW_SLEEP = 0x0002;
+
+ /**
+ * Force device into car mode, like it had been placed in the car dock.
+ * This will cause the device to switch to the car home UI as part of
+ * the mode switch.
+ * @param flags Must be 0.
+ */
+ public void enableCarMode(int flags) {
+ if (mService != null) {
+ try {
+ mService.enableCarMode(flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Flag for use with {@link #disableCarMode(int)}: go to the normal
+ * home activity as part of the disable. Disabling this way ensures
+ * a clean transition between the current activity (in car mode) and
+ * the original home activity (which was typically last running without
+ * being in car mode).
+ */
+ public static final int DISABLE_CAR_MODE_GO_HOME = 0x0001;
+
+ /**
+ * Turn off special mode if currently in car mode.
+ * @param flags May be 0 or {@link #DISABLE_CAR_MODE_GO_HOME}.
+ */
+ public void disableCarMode(int flags) {
+ if (mService != null) {
+ try {
+ mService.disableCarMode(flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Return the current running mode type. May be one of
+ * {@link Configuration#UI_MODE_TYPE_NORMAL Configuration.UI_MODE_TYPE_NORMAL},
+ * {@link Configuration#UI_MODE_TYPE_DESK Configuration.UI_MODE_TYPE_DESK},
+ * {@link Configuration#UI_MODE_TYPE_CAR Configuration.UI_MODE_TYPE_CAR},
+ * {@link Configuration#UI_MODE_TYPE_TELEVISION Configuration.UI_MODE_TYPE_TELEVISION},
+ * {@link Configuration#UI_MODE_TYPE_APPLIANCE Configuration.UI_MODE_TYPE_APPLIANCE},
+ * {@link Configuration#UI_MODE_TYPE_WATCH Configuration.UI_MODE_TYPE_WATCH}, or
+ * {@link Configuration#UI_MODE_TYPE_VR_HEADSET Configuration.UI_MODE_TYPE_VR_HEADSET}.
+ */
+ public int getCurrentModeType() {
+ if (mService != null) {
+ try {
+ return mService.getCurrentModeType();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return Configuration.UI_MODE_TYPE_NORMAL;
+ }
+
+ /**
+ * Sets the night mode.
+ * <p>
+ * The mode can be one of:
+ * <ul>
+ * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+ * {@code notnight} mode</li>
+ * <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
+ * {@code night} mode</li>
+ * <li><em>{@link #MODE_NIGHT_AUTO}</em> automatically switches between
+ * {@code night} and {@code notnight} based on the device's current
+ * location and certain other sensors</li>
+ * </ul>
+ * <p>
+ * <strong>Note:</strong> On API 22 and below, changes to the night mode
+ * are only effective when the {@link Configuration#UI_MODE_TYPE_CAR car}
+ * or {@link Configuration#UI_MODE_TYPE_DESK desk} mode is enabled on a
+ * device. Starting in API 23, changes to night mode are always effective.
+ *
+ * @param mode the night mode to set
+ * @see #getNightMode()
+ */
+ public void setNightMode(@NightMode int mode) {
+ if (mService != null) {
+ try {
+ mService.setNightMode(mode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the currently configured night mode.
+ * <p>
+ * May be one of:
+ * <ul>
+ * <li>{@link #MODE_NIGHT_NO}</li>
+ * <li>{@link #MODE_NIGHT_YES}</li>
+ * <li>{@link #MODE_NIGHT_AUTO}</li>
+ * <li>{@code -1} on error</li>
+ * </ul>
+ *
+ * @return the current night mode, or {@code -1} on error
+ * @see #setNightMode(int)
+ */
+ public @NightMode int getNightMode() {
+ if (mService != null) {
+ try {
+ return mService.getNightMode();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return If UI mode is locked or not. When UI mode is locked, calls to change UI mode
+ * like {@link #enableCarMode(int)} will silently fail.
+ * @hide
+ */
+ @TestApi
+ public boolean isUiModeLocked() {
+ if (mService != null) {
+ try {
+ return mService.isUiModeLocked();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether night mode is locked or not.
+ * <p>
+ * When night mode is locked, only privileged system components may change
+ * night mode and calls from non-privileged applications to change night
+ * mode will fail silently.
+ *
+ * @return {@code true} if night mode is locked or {@code false} otherwise
+ * @hide
+ */
+ @TestApi
+ public boolean isNightModeLocked() {
+ if (mService != null) {
+ try {
+ return mService.isNightModeLocked();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/app/UserSwitchObserver.java b/android/app/UserSwitchObserver.java
new file mode 100644
index 00000000..c0f7a4cd
--- /dev/null
+++ b/android/app/UserSwitchObserver.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 android.app;
+
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+
+/**
+ * @hide
+ */
+public class UserSwitchObserver extends IUserSwitchObserver.Stub {
+ @Override
+ public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
+ if (reply != null) {
+ reply.sendResult(null);
+ }
+ }
+
+ @Override
+ public void onUserSwitchComplete(int newUserId) throws RemoteException {}
+
+ @Override
+ public void onForegroundProfileSwitch(int newProfileId) throws RemoteException {}
+
+ @Override
+ public void onLockedBootComplete(int newUserId) throws RemoteException {}
+} \ No newline at end of file
diff --git a/android/app/VoiceInteractor.java b/android/app/VoiceInteractor.java
new file mode 100644
index 00000000..823c4271
--- /dev/null
+++ b/android/app/VoiceInteractor.java
@@ -0,0 +1,1069 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.DebugUtils;
+import android.util.Log;
+import com.android.internal.app.IVoiceInteractor;
+import com.android.internal.app.IVoiceInteractorCallback;
+import com.android.internal.app.IVoiceInteractorRequest;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Interface for an {@link Activity} to interact with the user through voice. Use
+ * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor}
+ * to retrieve the interface, if the activity is currently involved in a voice interaction.
+ *
+ * <p>The voice interactor revolves around submitting voice interaction requests to the
+ * back-end voice interaction service that is working with the user. These requests are
+ * submitted with {@link #submitRequest}, providing a new instance of a
+ * {@link Request} subclass describing the type of operation to perform -- currently the
+ * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}.
+ *
+ * <p>Once a request is submitted, the voice system will process it and eventually deliver
+ * the result to the request object. The application can cancel a pending request at any
+ * time.
+ *
+ * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that
+ * if an activity is being restarted with retained state, it will retain the current
+ * VoiceInteractor and any outstanding requests. Because of this, you should always use
+ * {@link Request#getActivity() Request.getActivity} to get back to the activity of a
+ * request, rather than holding on to the activity instance yourself, either explicitly
+ * or implicitly through a non-static inner class.
+ */
+public final class VoiceInteractor {
+ static final String TAG = "VoiceInteractor";
+ static final boolean DEBUG = false;
+
+ static final Request[] NO_REQUESTS = new Request[0];
+
+ final IVoiceInteractor mInteractor;
+
+ Context mContext;
+ Activity mActivity;
+ boolean mRetaining;
+
+ final HandlerCaller mHandlerCaller;
+ final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
+ @Override
+ public void executeMessage(Message msg) {
+ SomeArgs args = (SomeArgs)msg.obj;
+ Request request;
+ boolean complete;
+ switch (msg.what) {
+ case MSG_CONFIRMATION_RESULT:
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
+ if (DEBUG) Log.d(TAG, "onConfirmResult: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
+ + " confirmed=" + msg.arg1 + " result=" + args.arg2);
+ if (request != null) {
+ ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
+ (Bundle) args.arg2);
+ request.clear();
+ }
+ break;
+ case MSG_PICK_OPTION_RESULT:
+ complete = msg.arg1 != 0;
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
+ if (DEBUG) Log.d(TAG, "onPickOptionResult: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
+ + " finished=" + complete + " selection=" + args.arg2
+ + " result=" + args.arg3);
+ if (request != null) {
+ ((PickOptionRequest)request).onPickOptionResult(complete,
+ (PickOptionRequest.Option[]) args.arg2, (Bundle) args.arg3);
+ if (complete) {
+ request.clear();
+ }
+ }
+ break;
+ case MSG_COMPLETE_VOICE_RESULT:
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
+ if (DEBUG) Log.d(TAG, "onCompleteVoice: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
+ + " result=" + args.arg2);
+ if (request != null) {
+ ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2);
+ request.clear();
+ }
+ break;
+ case MSG_ABORT_VOICE_RESULT:
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
+ if (DEBUG) Log.d(TAG, "onAbortVoice: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
+ + " result=" + args.arg2);
+ if (request != null) {
+ ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2);
+ request.clear();
+ }
+ break;
+ case MSG_COMMAND_RESULT:
+ complete = msg.arg1 != 0;
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
+ if (DEBUG) Log.d(TAG, "onCommandResult: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
+ + " completed=" + msg.arg1 + " result=" + args.arg2);
+ if (request != null) {
+ ((CommandRequest)request).onCommandResult(msg.arg1 != 0,
+ (Bundle) args.arg2);
+ if (complete) {
+ request.clear();
+ }
+ }
+ break;
+ case MSG_CANCEL_RESULT:
+ request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
+ if (DEBUG) Log.d(TAG, "onCancelResult: req="
+ + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
+ if (request != null) {
+ request.onCancel();
+ request.clear();
+ }
+ break;
+ }
+ }
+ };
+
+ final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
+ @Override
+ public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean finished,
+ Bundle result) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
+ MSG_CONFIRMATION_RESULT, finished ? 1 : 0, request, result));
+ }
+
+ @Override
+ public void deliverPickOptionResult(IVoiceInteractorRequest request,
+ boolean finished, PickOptionRequest.Option[] options, Bundle result) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO(
+ MSG_PICK_OPTION_RESULT, finished ? 1 : 0, request, options, result));
+ }
+
+ @Override
+ public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
+ MSG_COMPLETE_VOICE_RESULT, request, result));
+ }
+
+ @Override
+ public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
+ MSG_ABORT_VOICE_RESULT, request, result));
+ }
+
+ @Override
+ public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
+ Bundle result) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
+ MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
+ }
+
+ @Override
+ public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
+ MSG_CANCEL_RESULT, request, null));
+ }
+ };
+
+ final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<>();
+
+ static final int MSG_CONFIRMATION_RESULT = 1;
+ static final int MSG_PICK_OPTION_RESULT = 2;
+ static final int MSG_COMPLETE_VOICE_RESULT = 3;
+ static final int MSG_ABORT_VOICE_RESULT = 4;
+ static final int MSG_COMMAND_RESULT = 5;
+ static final int MSG_CANCEL_RESULT = 6;
+
+ /**
+ * Base class for voice interaction requests that can be submitted to the interactor.
+ * Do not instantiate this directly -- instead, use the appropriate subclass.
+ */
+ public static abstract class Request {
+ IVoiceInteractorRequest mRequestInterface;
+ Context mContext;
+ Activity mActivity;
+ String mName;
+
+ Request() {
+ }
+
+ /**
+ * Return the name this request was submitted through
+ * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Cancel this active request.
+ */
+ public void cancel() {
+ if (mRequestInterface == null) {
+ throw new IllegalStateException("Request " + this + " is no longer active");
+ }
+ try {
+ mRequestInterface.cancel();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Voice interactor has died", e);
+ }
+ }
+
+ /**
+ * Return the current {@link Context} this request is associated with. May change
+ * if the activity hosting it goes through a configuration change.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the current {@link Activity} this request is associated with. Will change
+ * if the activity is restarted such as through a configuration change.
+ */
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Report from voice interaction service: this operation has been canceled, typically
+ * as a completion of a previous call to {@link #cancel} or when the user explicitly
+ * cancelled.
+ */
+ public void onCancel() {
+ }
+
+ /**
+ * The request is now attached to an activity, or being re-attached to a new activity
+ * after a configuration change.
+ */
+ public void onAttached(Activity activity) {
+ }
+
+ /**
+ * The request is being detached from an activity.
+ */
+ public void onDetached() {
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ DebugUtils.buildShortClassTag(this, sb);
+ sb.append(" ");
+ sb.append(getRequestTypeName());
+ sb.append(" name=");
+ sb.append(mName);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mRequestInterface=");
+ writer.println(mRequestInterface.asBinder());
+ writer.print(prefix); writer.print("mActivity="); writer.println(mActivity);
+ writer.print(prefix); writer.print("mName="); writer.println(mName);
+ }
+
+ String getRequestTypeName() {
+ return "Request";
+ }
+
+ void clear() {
+ mRequestInterface = null;
+ mContext = null;
+ mActivity = null;
+ mName = null;
+ }
+
+ abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
+ String packageName, IVoiceInteractorCallback callback) throws RemoteException;
+ }
+
+ /**
+ * Confirms an operation with the user via the trusted system
+ * VoiceInteractionService. This allows an Activity to complete an unsafe operation that
+ * would require the user to touch the screen when voice interaction mode is not enabled.
+ * The result of the confirmation will be returned through an asynchronous call to
+ * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
+ * {@link #onCancel()} - these methods should be overridden to define the application specific
+ * behavior.
+ *
+ * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
+ * include context information about how the action will be completed
+ * (e.g. booking a cab might include details about how long until the cab arrives)
+ * so the user can give a confirmation.
+ */
+ public static class ConfirmationRequest extends Request {
+ final Prompt mPrompt;
+ final Bundle mExtras;
+
+ /**
+ * Create a new confirmation request.
+ * @param prompt Optional confirmation to speak to the user or null if nothing
+ * should be spoken.
+ * @param extras Additional optional information or null.
+ */
+ public ConfirmationRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
+ mPrompt = prompt;
+ mExtras = extras;
+ }
+
+ /**
+ * Create a new confirmation request.
+ * @param prompt Optional confirmation to speak to the user or null if nothing
+ * should be spoken.
+ * @param extras Additional optional information or null.
+ * @hide
+ */
+ public ConfirmationRequest(CharSequence prompt, Bundle extras) {
+ mPrompt = (prompt != null ? new Prompt(prompt) : null);
+ mExtras = extras;
+ }
+
+ /**
+ * Handle the confirmation result. Override this method to define
+ * the behavior when the user confirms or rejects the operation.
+ * @param confirmed Whether the user confirmed or rejected the operation.
+ * @param result Additional result information or null.
+ */
+ public void onConfirmationResult(boolean confirmed, Bundle result) {
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
+ if (mExtras != null) {
+ writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
+ }
+ }
+
+ String getRequestTypeName() {
+ return "Confirmation";
+ }
+
+ IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
+ IVoiceInteractorCallback callback) throws RemoteException {
+ return interactor.startConfirmation(packageName, callback, mPrompt, mExtras);
+ }
+ }
+
+ /**
+ * Select a single option from multiple potential options with the user via the trusted system
+ * VoiceInteractionService. Typically, the application would present this visually as
+ * a list view to allow selecting the option by touch.
+ * The result of the confirmation will be returned through an asynchronous call to
+ * either {@link #onPickOptionResult} or {@link #onCancel()} - these methods should
+ * be overridden to define the application specific behavior.
+ */
+ public static class PickOptionRequest extends Request {
+ final Prompt mPrompt;
+ final Option[] mOptions;
+ final Bundle mExtras;
+
+ /**
+ * Represents a single option that the user may select using their voice. The
+ * {@link #getIndex()} method should be used as a unique ID to identify the option
+ * when it is returned from the voice interactor.
+ */
+ public static final class Option implements Parcelable {
+ final CharSequence mLabel;
+ final int mIndex;
+ ArrayList<CharSequence> mSynonyms;
+ Bundle mExtras;
+
+ /**
+ * Creates an option that a user can select with their voice by matching the label
+ * or one of several synonyms.
+ * @param label The label that will both be matched against what the user speaks
+ * and displayed visually.
+ * @hide
+ */
+ public Option(CharSequence label) {
+ mLabel = label;
+ mIndex = -1;
+ }
+
+ /**
+ * Creates an option that a user can select with their voice by matching the label
+ * or one of several synonyms.
+ * @param label The label that will both be matched against what the user speaks
+ * and displayed visually.
+ * @param index The location of this option within the overall set of options.
+ * Can be used to help identify the option when it is returned from the
+ * voice interactor.
+ */
+ public Option(CharSequence label, int index) {
+ mLabel = label;
+ mIndex = index;
+ }
+
+ /**
+ * Add a synonym term to the option to indicate an alternative way the content
+ * may be matched.
+ * @param synonym The synonym that will be matched against what the user speaks,
+ * but not displayed.
+ */
+ public Option addSynonym(CharSequence synonym) {
+ if (mSynonyms == null) {
+ mSynonyms = new ArrayList<>();
+ }
+ mSynonyms.add(synonym);
+ return this;
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Return the index that was supplied in the constructor.
+ * If the option was constructed without an index, -1 is returned.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public int countSynonyms() {
+ return mSynonyms != null ? mSynonyms.size() : 0;
+ }
+
+ public CharSequence getSynonymAt(int index) {
+ return mSynonyms != null ? mSynonyms.get(index) : null;
+ }
+
+ /**
+ * Set optional extra information associated with this option. Note that this
+ * method takes ownership of the supplied extras Bundle.
+ */
+ public void setExtras(Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
+ * Return any optional extras information associated with this option, or null
+ * if there is none. Note that this method returns a reference to the actual
+ * extras Bundle in the option, so modifications to it will directly modify the
+ * extras in the option.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ Option(Parcel in) {
+ mLabel = in.readCharSequence();
+ mIndex = in.readInt();
+ mSynonyms = in.readCharSequenceList();
+ mExtras = in.readBundle();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mLabel);
+ dest.writeInt(mIndex);
+ dest.writeCharSequenceList(mSynonyms);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final Parcelable.Creator<Option> CREATOR
+ = new Parcelable.Creator<Option>() {
+ public Option createFromParcel(Parcel in) {
+ return new Option(in);
+ }
+
+ public Option[] newArray(int size) {
+ return new Option[size];
+ }
+ };
+ };
+
+ /**
+ * Create a new pick option request.
+ * @param prompt Optional question to be asked of the user when the options are
+ * presented or null if nothing should be asked.
+ * @param options The set of {@link Option}s the user is selecting from.
+ * @param extras Additional optional information or null.
+ */
+ public PickOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ mPrompt = prompt;
+ mOptions = options;
+ mExtras = extras;
+ }
+
+ /**
+ * Create a new pick option request.
+ * @param prompt Optional question to be asked of the user when the options are
+ * presented or null if nothing should be asked.
+ * @param options The set of {@link Option}s the user is selecting from.
+ * @param extras Additional optional information or null.
+ * @hide
+ */
+ public PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras) {
+ mPrompt = (prompt != null ? new Prompt(prompt) : null);
+ mOptions = options;
+ mExtras = extras;
+ }
+
+ /**
+ * Called when a single option is confirmed or narrowed to one of several options. Override
+ * this method to define the behavior when the user selects an option or narrows down the
+ * set of options.
+ * @param finished True if the voice interaction has finished making a selection, in
+ * which case {@code selections} contains the final result. If false, this request is
+ * still active and you will continue to get calls on it.
+ * @param selections Either a single {@link Option} or one of several {@link Option}s the
+ * user has narrowed the choices down to.
+ * @param result Additional optional information.
+ */
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
+ if (mOptions != null) {
+ writer.print(prefix); writer.println("Options:");
+ for (int i=0; i<mOptions.length; i++) {
+ Option op = mOptions[i];
+ writer.print(prefix); writer.print(" #"); writer.print(i); writer.println(":");
+ writer.print(prefix); writer.print(" mLabel="); writer.println(op.mLabel);
+ writer.print(prefix); writer.print(" mIndex="); writer.println(op.mIndex);
+ if (op.mSynonyms != null && op.mSynonyms.size() > 0) {
+ writer.print(prefix); writer.println(" Synonyms:");
+ for (int j=0; j<op.mSynonyms.size(); j++) {
+ writer.print(prefix); writer.print(" #"); writer.print(j);
+ writer.print(": "); writer.println(op.mSynonyms.get(j));
+ }
+ }
+ if (op.mExtras != null) {
+ writer.print(prefix); writer.print(" mExtras=");
+ writer.println(op.mExtras);
+ }
+ }
+ }
+ if (mExtras != null) {
+ writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
+ }
+ }
+
+ String getRequestTypeName() {
+ return "PickOption";
+ }
+
+ IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
+ IVoiceInteractorCallback callback) throws RemoteException {
+ return interactor.startPickOption(packageName, callback, mPrompt, mOptions, mExtras);
+ }
+ }
+
+ /**
+ * Reports that the current interaction was successfully completed with voice, so the
+ * application can report the final status to the user. When the response comes back, the
+ * voice system has handled the request and is ready to switch; at that point the
+ * application can start a new non-voice activity or finish. Be sure when starting the new
+ * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
+ * interaction task.
+ */
+ public static class CompleteVoiceRequest extends Request {
+ final Prompt mPrompt;
+ final Bundle mExtras;
+
+ /**
+ * Create a new completed voice interaction request.
+ * @param prompt Optional message to speak to the user about the completion status of
+ * the task or null if nothing should be spoken.
+ * @param extras Additional optional information or null.
+ */
+ public CompleteVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
+ mPrompt = prompt;
+ mExtras = extras;
+ }
+
+ /**
+ * Create a new completed voice interaction request.
+ * @param message Optional message to speak to the user about the completion status of
+ * the task or null if nothing should be spoken.
+ * @param extras Additional optional information or null.
+ * @hide
+ */
+ public CompleteVoiceRequest(CharSequence message, Bundle extras) {
+ mPrompt = (message != null ? new Prompt(message) : null);
+ mExtras = extras;
+ }
+
+ public void onCompleteResult(Bundle result) {
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
+ if (mExtras != null) {
+ writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
+ }
+ }
+
+ String getRequestTypeName() {
+ return "CompleteVoice";
+ }
+
+ IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
+ IVoiceInteractorCallback callback) throws RemoteException {
+ return interactor.startCompleteVoice(packageName, callback, mPrompt, mExtras);
+ }
+ }
+
+ /**
+ * Reports that the current interaction can not be complete with voice, so the
+ * application will need to switch to a traditional input UI. Applications should
+ * only use this when they need to completely bail out of the voice interaction
+ * and switch to a traditional UI. When the response comes back, the voice
+ * system has handled the request and is ready to switch; at that point the application
+ * can start a new non-voice activity. Be sure when starting the new activity
+ * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
+ * interaction task.
+ */
+ public static class AbortVoiceRequest extends Request {
+ final Prompt mPrompt;
+ final Bundle mExtras;
+
+ /**
+ * Create a new voice abort request.
+ * @param prompt Optional message to speak to the user indicating why the task could
+ * not be completed by voice or null if nothing should be spoken.
+ * @param extras Additional optional information or null.
+ */
+ public AbortVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
+ mPrompt = prompt;
+ mExtras = extras;
+ }
+
+ /**
+ * Create a new voice abort request.
+ * @param message Optional message to speak to the user indicating why the task could
+ * not be completed by voice or null if nothing should be spoken.
+ * @param extras Additional optional information or null.
+ * @hide
+ */
+ public AbortVoiceRequest(CharSequence message, Bundle extras) {
+ mPrompt = (message != null ? new Prompt(message) : null);
+ mExtras = extras;
+ }
+
+ public void onAbortResult(Bundle result) {
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
+ if (mExtras != null) {
+ writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
+ }
+ }
+
+ String getRequestTypeName() {
+ return "AbortVoice";
+ }
+
+ IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
+ IVoiceInteractorCallback callback) throws RemoteException {
+ return interactor.startAbortVoice(packageName, callback, mPrompt, mExtras);
+ }
+ }
+
+ /**
+ * Execute a vendor-specific command using the trusted system VoiceInteractionService.
+ * This allows an Activity to request additional information from the user needed to
+ * complete an action (e.g. booking a table might have several possible times that the
+ * user could select from or an app might need the user to agree to a terms of service).
+ * The result of the confirmation will be returned through an asynchronous call to
+ * either {@link #onCommandResult(boolean, android.os.Bundle)} or
+ * {@link #onCancel()}.
+ *
+ * <p>The command is a string that describes the generic operation to be performed.
+ * The command will determine how the properties in extras are interpreted and the set of
+ * available commands is expected to grow over time. An example might be
+ * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
+ * airline check-in. (This is not an actual working example.)
+ */
+ public static class CommandRequest extends Request {
+ final String mCommand;
+ final Bundle mArgs;
+
+ /**
+ * Create a new generic command request.
+ * @param command The desired command to perform.
+ * @param args Additional arguments to control execution of the command.
+ */
+ public CommandRequest(String command, Bundle args) {
+ mCommand = command;
+ mArgs = args;
+ }
+
+ /**
+ * Results for CommandRequest can be returned in partial chunks.
+ * The isCompleted is set to true iff all results have been returned, indicating the
+ * CommandRequest has completed.
+ */
+ public void onCommandResult(boolean isCompleted, Bundle result) {
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mCommand="); writer.println(mCommand);
+ if (mArgs != null) {
+ writer.print(prefix); writer.print("mArgs="); writer.println(mArgs);
+ }
+ }
+
+ String getRequestTypeName() {
+ return "Command";
+ }
+
+ IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
+ IVoiceInteractorCallback callback) throws RemoteException {
+ return interactor.startCommand(packageName, callback, mCommand, mArgs);
+ }
+ }
+
+ /**
+ * A set of voice prompts to use with the voice interaction system to confirm an action, select
+ * an option, or do similar operations. Multiple voice prompts may be provided for variety. A
+ * visual prompt must be provided, which might not match the spoken version. For example, the
+ * confirmation "Are you sure you want to purchase this item?" might use a visual label like
+ * "Purchase item".
+ */
+ public static class Prompt implements Parcelable {
+ // Mandatory voice prompt. Must contain at least one item, which must not be null.
+ private final CharSequence[] mVoicePrompts;
+
+ // Mandatory visual prompt.
+ private final CharSequence mVisualPrompt;
+
+ /**
+ * Constructs a prompt set.
+ * @param voicePrompts An array of one or more voice prompts. Must not be empty or null.
+ * @param visualPrompt A prompt to display on the screen. Must not be null.
+ */
+ public Prompt(@NonNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt) {
+ if (voicePrompts == null) {
+ throw new NullPointerException("voicePrompts must not be null");
+ }
+ if (voicePrompts.length == 0) {
+ throw new IllegalArgumentException("voicePrompts must not be empty");
+ }
+ if (visualPrompt == null) {
+ throw new NullPointerException("visualPrompt must not be null");
+ }
+ this.mVoicePrompts = voicePrompts;
+ this.mVisualPrompt = visualPrompt;
+ }
+
+ /**
+ * Constructs a prompt set with single prompt used for all interactions. This is most useful
+ * in test apps. Non-trivial apps should prefer the detailed constructor.
+ */
+ public Prompt(@NonNull CharSequence prompt) {
+ this.mVoicePrompts = new CharSequence[] { prompt };
+ this.mVisualPrompt = prompt;
+ }
+
+ /**
+ * Returns a prompt to use for voice interactions.
+ */
+ @NonNull
+ public CharSequence getVoicePromptAt(int index) {
+ return mVoicePrompts[index];
+ }
+
+ /**
+ * Returns the number of different voice prompts.
+ */
+ public int countVoicePrompts() {
+ return mVoicePrompts.length;
+ }
+
+ /**
+ * Returns the prompt to use for visual display.
+ */
+ @NonNull
+ public CharSequence getVisualPrompt() {
+ return mVisualPrompt;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ DebugUtils.buildShortClassTag(this, sb);
+ if (mVisualPrompt != null && mVoicePrompts != null && mVoicePrompts.length == 1
+ && mVisualPrompt.equals(mVoicePrompts[0])) {
+ sb.append(" ");
+ sb.append(mVisualPrompt);
+ } else {
+ if (mVisualPrompt != null) {
+ sb.append(" visual="); sb.append(mVisualPrompt);
+ }
+ if (mVoicePrompts != null) {
+ sb.append(", voice=");
+ for (int i=0; i<mVoicePrompts.length; i++) {
+ if (i > 0) sb.append(" | ");
+ sb.append(mVoicePrompts[i]);
+ }
+ }
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /** Constructor to support Parcelable behavior. */
+ Prompt(Parcel in) {
+ mVoicePrompts = in.readCharSequenceArray();
+ mVisualPrompt = in.readCharSequence();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequenceArray(mVoicePrompts);
+ dest.writeCharSequence(mVisualPrompt);
+ }
+
+ public static final Creator<Prompt> CREATOR
+ = new Creator<Prompt>() {
+ public Prompt createFromParcel(Parcel in) {
+ return new Prompt(in);
+ }
+
+ public Prompt[] newArray(int size) {
+ return new Prompt[size];
+ }
+ };
+ }
+
+ VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity,
+ Looper looper) {
+ mInteractor = interactor;
+ mContext = context;
+ mActivity = activity;
+ mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
+ }
+
+ Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
+ synchronized (mActiveRequests) {
+ Request req = mActiveRequests.get(request.asBinder());
+ if (req != null && complete) {
+ mActiveRequests.remove(request.asBinder());
+ }
+ return req;
+ }
+ }
+
+ private ArrayList<Request> makeRequestList() {
+ final int N = mActiveRequests.size();
+ if (N < 1) {
+ return null;
+ }
+ ArrayList<Request> list = new ArrayList<>(N);
+ for (int i=0; i<N; i++) {
+ list.add(mActiveRequests.valueAt(i));
+ }
+ return list;
+ }
+
+ void attachActivity(Activity activity) {
+ mRetaining = false;
+ if (mActivity == activity) {
+ return;
+ }
+ mContext = activity;
+ mActivity = activity;
+ ArrayList<Request> reqs = makeRequestList();
+ if (reqs != null) {
+ for (int i=0; i<reqs.size(); i++) {
+ Request req = reqs.get(i);
+ req.mContext = activity;
+ req.mActivity = activity;
+ req.onAttached(activity);
+ }
+ }
+ }
+
+ void retainInstance() {
+ mRetaining = true;
+ }
+
+ void detachActivity() {
+ ArrayList<Request> reqs = makeRequestList();
+ if (reqs != null) {
+ for (int i=0; i<reqs.size(); i++) {
+ Request req = reqs.get(i);
+ req.onDetached();
+ req.mActivity = null;
+ req.mContext = null;
+ }
+ }
+ if (!mRetaining) {
+ reqs = makeRequestList();
+ if (reqs != null) {
+ for (int i=0; i<reqs.size(); i++) {
+ Request req = reqs.get(i);
+ req.cancel();
+ }
+ }
+ mActiveRequests.clear();
+ }
+ mContext = null;
+ mActivity = null;
+ }
+
+ public boolean submitRequest(Request request) {
+ return submitRequest(request, null);
+ }
+
+ /**
+ * Submit a new {@link Request} to the voice interaction service. The request must be
+ * one of the available subclasses -- {@link ConfirmationRequest}, {@link PickOptionRequest},
+ * {@link CompleteVoiceRequest}, {@link AbortVoiceRequest}, or {@link CommandRequest}.
+ *
+ * @param request The desired request to submit.
+ * @param name An optional name for this request, or null. This can be used later with
+ * {@link #getActiveRequests} and {@link #getActiveRequest} to find the request.
+ *
+ * @return Returns true of the request was successfully submitted, else false.
+ */
+ public boolean submitRequest(Request request, String name) {
+ try {
+ if (request.mRequestInterface != null) {
+ throw new IllegalStateException("Given " + request + " is already active");
+ }
+ IVoiceInteractorRequest ireq = request.submit(mInteractor,
+ mContext.getOpPackageName(), mCallback);
+ request.mRequestInterface = ireq;
+ request.mContext = mContext;
+ request.mActivity = mActivity;
+ request.mName = name;
+ synchronized (mActiveRequests) {
+ mActiveRequests.put(ireq.asBinder(), request);
+ }
+ return true;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Remove voice interactor service died", e);
+ return false;
+ }
+ }
+
+ /**
+ * Return all currently active requests.
+ */
+ public Request[] getActiveRequests() {
+ synchronized (mActiveRequests) {
+ final int N = mActiveRequests.size();
+ if (N <= 0) {
+ return NO_REQUESTS;
+ }
+ Request[] requests = new Request[N];
+ for (int i=0; i<N; i++) {
+ requests[i] = mActiveRequests.valueAt(i);
+ }
+ return requests;
+ }
+ }
+
+ /**
+ * Return any currently active request that was submitted with the given name.
+ *
+ * @param name The name used to submit the request, as per
+ * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
+ * @return Returns the active request with that name, or null if there was none.
+ */
+ public Request getActiveRequest(String name) {
+ synchronized (mActiveRequests) {
+ final int N = mActiveRequests.size();
+ for (int i=0; i<N; i++) {
+ Request req = mActiveRequests.valueAt(i);
+ if (name == req.getName() || (name != null && name.equals(req.getName()))) {
+ return req;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Queries the supported commands available from the VoiceInteractionService.
+ * The command is a string that describes the generic operation to be performed.
+ * An example might be "org.example.commands.PICK_DATE" to ask the user to pick
+ * a date. (Note: This is not an actual working example.)
+ *
+ * @param commands The array of commands to query for support.
+ * @return Array of booleans indicating whether each command is supported or not.
+ */
+ public boolean[] supportsCommands(String[] commands) {
+ try {
+ boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
+ if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
+ return res;
+ } catch (RemoteException e) {
+ throw new RuntimeException("Voice interactor has died", e);
+ }
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ String innerPrefix = prefix + " ";
+ if (mActiveRequests.size() > 0) {
+ writer.print(prefix); writer.println("Active voice requests:");
+ for (int i=0; i<mActiveRequests.size(); i++) {
+ Request req = mActiveRequests.valueAt(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": ");
+ writer.println(req);
+ req.dump(innerPrefix, fd, writer, args);
+ }
+ }
+ writer.print(prefix); writer.println("VoiceInteractor misc state:");
+ writer.print(prefix); writer.print(" mInteractor=");
+ writer.println(mInteractor.asBinder());
+ writer.print(prefix); writer.print(" mActivity="); writer.println(mActivity);
+ }
+}
diff --git a/android/app/Vr2dDisplayProperties.java b/android/app/Vr2dDisplayProperties.java
new file mode 100644
index 00000000..0eb2af36
--- /dev/null
+++ b/android/app/Vr2dDisplayProperties.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.PrintWriter;
+
+/**
+ * Display properties to be used by VR mode when creating a virtual display.
+ *
+ * @hide
+ */
+public final class Vr2dDisplayProperties implements Parcelable {
+
+ public static final int FLAG_VIRTUAL_DISPLAY_ENABLED = 1;
+
+ /**
+ * The actual width, height and dpi.
+ */
+ private final int mWidth;
+ private final int mHeight;
+ private final int mDpi;
+
+ // Flags describing the virtual display behavior.
+ private final int mAddedFlags;
+ private final int mRemovedFlags;
+
+ public Vr2dDisplayProperties(int width, int height, int dpi) {
+ this(width, height, dpi, 0, 0);
+ }
+
+ private Vr2dDisplayProperties(int width, int height, int dpi, int flags, int removedFlags) {
+ mWidth = width;
+ mHeight = height;
+ mDpi = dpi;
+ mAddedFlags = flags;
+ mRemovedFlags = removedFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getWidth();
+ result = 31 * result + getHeight();
+ result = 31 * result + getDpi();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Vr2dDisplayProperties{"
+ + "mWidth=" + mWidth
+ + ", mHeight=" + mHeight
+ + ", mDpi=" + mDpi
+ + ", flags=" + toReadableFlags(mAddedFlags)
+ + ", removed_flags=" + toReadableFlags(mRemovedFlags)
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Vr2dDisplayProperties that = (Vr2dDisplayProperties) o;
+
+ if (getFlags() != that.getFlags()) return false;
+ if (getRemovedFlags() != that.getRemovedFlags()) return false;
+ if (getWidth() != that.getWidth()) return false;
+ if (getHeight() != that.getHeight()) return false;
+ return getDpi() == that.getDpi();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ dest.writeInt(mDpi);
+ dest.writeInt(mAddedFlags);
+ dest.writeInt(mRemovedFlags);
+ }
+
+ public static final Parcelable.Creator<Vr2dDisplayProperties> CREATOR
+ = new Parcelable.Creator<Vr2dDisplayProperties>() {
+ @Override
+ public Vr2dDisplayProperties createFromParcel(Parcel source) {
+ return new Vr2dDisplayProperties(source);
+ }
+
+ @Override
+ public Vr2dDisplayProperties[] newArray(int size) {
+ return new Vr2dDisplayProperties[size];
+ }
+ };
+
+ private Vr2dDisplayProperties(Parcel source) {
+ mWidth = source.readInt();
+ mHeight = source.readInt();
+ mDpi = source.readInt();
+ mAddedFlags = source.readInt();
+ mRemovedFlags = source.readInt();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + toString());
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getDpi() {
+ return mDpi;
+ }
+
+ public int getFlags() {
+ return mAddedFlags;
+ }
+
+ public int getRemovedFlags() {
+ return mRemovedFlags;
+ }
+
+ private static String toReadableFlags(int flags) {
+ String retval = "{";
+ if ((flags & FLAG_VIRTUAL_DISPLAY_ENABLED) == FLAG_VIRTUAL_DISPLAY_ENABLED) {
+ retval += "enabled";
+ }
+ return retval + "}";
+ }
+
+ /**
+ * Convenience class for creating Vr2dDisplayProperties.
+ */
+ public static class Builder {
+ private int mAddedFlags = 0;
+ private int mRemovedFlags = 0;
+
+ // Negative values are translated as an "ignore" to VrManagerService.
+ private int mWidth = -1;
+ private int mHeight = -1;
+ private int mDpi = -1;
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the dimensions to use for the virtual display.
+ */
+ public Builder setDimensions(int width, int height, int dpi) {
+ mWidth = width;
+ mHeight = height;
+ mDpi = dpi;
+ return this;
+ }
+
+ /**
+ * Toggles the virtual display functionality for 2D activities in VR.
+ */
+ public Builder setEnabled(boolean enabled) {
+ if (enabled) {
+ addFlags(FLAG_VIRTUAL_DISPLAY_ENABLED);
+ } else {
+ removeFlags(FLAG_VIRTUAL_DISPLAY_ENABLED);
+ }
+ return this;
+ }
+
+ /**
+ * Adds property flags.
+ */
+ public Builder addFlags(int flags) {
+ mAddedFlags |= flags;
+ mRemovedFlags &= ~flags;
+ return this;
+ }
+
+ /**
+ * Removes property flags.
+ */
+ public Builder removeFlags(int flags) {
+ mRemovedFlags |= flags;
+ mAddedFlags &= ~flags;
+ return this;
+ }
+
+ /**
+ * Builds the Vr2dDisplayProperty instance.
+ */
+ public Vr2dDisplayProperties build() {
+ return new Vr2dDisplayProperties(mWidth, mHeight, mDpi, mAddedFlags, mRemovedFlags);
+ }
+ }
+}
diff --git a/android/app/VrManager.java b/android/app/VrManager.java
new file mode 100644
index 00000000..363e20a7
--- /dev/null
+++ b/android/app/VrManager.java
@@ -0,0 +1,172 @@
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.service.vr.IPersistentVrStateCallbacks;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.util.ArrayMap;
+
+import java.util.Map;
+
+/**
+ * Used to control aspects of a devices Virtual Reality (VR) capabilities.
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.VR_SERVICE)
+public class VrManager {
+
+ private static class CallbackEntry {
+ final IVrStateCallbacks mStateCallback = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ mHandler.post(() -> mCallback.onVrStateChanged(enabled));
+ }
+
+ };
+ final IPersistentVrStateCallbacks mPersistentStateCallback =
+ new IPersistentVrStateCallbacks.Stub() {
+ @Override
+ public void onPersistentVrStateChanged(boolean enabled) {
+ mHandler.post(() -> mCallback.onPersistentVrStateChanged(enabled));
+ }
+ };
+ final VrStateCallback mCallback;
+ final Handler mHandler;
+
+ CallbackEntry(VrStateCallback callback, Handler handler) {
+ mCallback = callback;
+ mHandler = handler;
+ }
+ }
+
+ private final IVrManager mService;
+ private Map<VrStateCallback, CallbackEntry> mCallbackMap = new ArrayMap<>();
+
+ /**
+ * {@hide}
+ */
+ public VrManager(IVrManager service) {
+ mService = service;
+ }
+
+ /**
+ * Registers a callback to be notified of changes to the VR Mode state.
+ *
+ * @param callback The callback to register.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public void registerVrStateCallback(VrStateCallback callback, @NonNull Handler handler) {
+ if (callback == null || mCallbackMap.containsKey(callback)) {
+ return;
+ }
+
+ CallbackEntry entry = new CallbackEntry(callback, handler);
+ mCallbackMap.put(callback, entry);
+ try {
+ mService.registerListener(entry.mStateCallback);
+ mService.registerPersistentVrStateListener(entry.mPersistentStateCallback);
+ } catch (RemoteException e) {
+ try {
+ unregisterVrStateCallback(callback);
+ } catch (Exception ignore) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Deregisters VR State callbacks.
+ *
+ * @param callback The callback to deregister.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public void unregisterVrStateCallback(VrStateCallback callback) {
+ CallbackEntry entry = mCallbackMap.remove(callback);
+ if (entry != null) {
+ try {
+ mService.unregisterListener(entry.mStateCallback);
+ } catch (RemoteException ignore) {
+ // Dont rethrow exceptions from requests to unregister.
+ }
+
+ try {
+ mService.unregisterPersistentVrStateListener(entry.mPersistentStateCallback);
+ } catch (RemoteException ignore) {
+ // Dont rethrow exceptions from requests to unregister.
+ }
+ }
+ }
+
+ /**
+ * Returns the current VrMode state.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_VR_STATE)
+ public boolean getVrModeEnabled() {
+ try {
+ return mService.getVrModeState();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the current VrMode state.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_VR_STATE)
+ public boolean getPersistentVrModeEnabled() {
+ try {
+ return mService.getPersistentVrModeEnabled();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ /**
+ * Sets the persistent VR mode state of a device. When a device is in persistent VR mode it will
+ * remain in VR mode even if the foreground does not specify Vr mode being enabled. Mainly used
+ * by VR viewers to indicate that a device is placed in a VR viewer.
+ *
+ * @see Activity#setVrModeEnabled(boolean, ComponentName)
+ * @param enabled true if the device should be placed in persistent VR mode.
+ */
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public void setPersistentVrModeEnabled(boolean enabled) {
+ try {
+ mService.setPersistentVrModeEnabled(enabled);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the resolution and DPI of the vr2d virtual display used to display 2D
+ * applications in VR mode.
+ *
+ * @param vr2dDisplayProp properties to be set to the virtual display for
+ * 2D applications in VR mode.
+ *
+ * {@hide}
+ */
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public void setVr2dDisplayProperties(
+ Vr2dDisplayProperties vr2dDisplayProp) {
+ try {
+ mService.setVr2dDisplayProperties(vr2dDisplayProp);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/app/VrStateCallback.java b/android/app/VrStateCallback.java
new file mode 100644
index 00000000..742faa06
--- /dev/null
+++ b/android/app/VrStateCallback.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+/**
+ * Listens to VR Mode state changes. Use with methods in {@link VrManager}.
+ *
+ * @hide
+ */
+public abstract class VrStateCallback {
+
+ /**
+ * Callback triggered when there is a change to Persistent VR State.
+ *
+ * @param enabled True when VR State is in persistent mode, false otherwise.
+ */
+ public void onPersistentVrStateChanged(boolean enabled) {}
+
+ /**
+ * Callback triggered when there is a change to Vr State.
+ *
+ * @param enabled True when VR State is in VR mode, false otherwise.
+ */
+ public void onVrStateChanged(boolean enabled) {}
+}
diff --git a/android/app/WaitResult.java b/android/app/WaitResult.java
new file mode 100644
index 00000000..898d0cab
--- /dev/null
+++ b/android/app/WaitResult.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.app;
+
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.PrintWriter;
+
+/**
+ * Information returned after waiting for an activity start.
+ *
+ * @hide
+ */
+public class WaitResult implements Parcelable {
+ public int result;
+ public boolean timeout;
+ public ComponentName who;
+ public long thisTime;
+ public long totalTime;
+
+ public WaitResult() {
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(result);
+ dest.writeInt(timeout ? 1 : 0);
+ ComponentName.writeToParcel(who, dest);
+ dest.writeLong(thisTime);
+ dest.writeLong(totalTime);
+ }
+
+ public static final Parcelable.Creator<WaitResult> CREATOR
+ = new Parcelable.Creator<WaitResult>() {
+ @Override
+ public WaitResult createFromParcel(Parcel source) {
+ return new WaitResult(source);
+ }
+
+ @Override
+ public WaitResult[] newArray(int size) {
+ return new WaitResult[size];
+ }
+ };
+
+ private WaitResult(Parcel source) {
+ result = source.readInt();
+ timeout = source.readInt() != 0;
+ who = ComponentName.readFromParcel(source);
+ thisTime = source.readLong();
+ totalTime = source.readLong();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "WaitResult:");
+ pw.println(prefix + " result=" + result);
+ pw.println(prefix + " timeout=" + timeout);
+ pw.println(prefix + " who=" + who);
+ pw.println(prefix + " thisTime=" + thisTime);
+ pw.println(prefix + " totalTime=" + totalTime);
+ }
+} \ No newline at end of file
diff --git a/android/app/WallpaperColors.java b/android/app/WallpaperColors.java
new file mode 100644
index 00000000..a2864b9d
--- /dev/null
+++ b/android/app/WallpaperColors.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Size;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.graphics.palette.Palette;
+import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides information about the colors of a wallpaper.
+ * <p>
+ * Exposes the 3 most visually representative colors of a wallpaper. Can be either
+ * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()}
+ * or {@link WallpaperColors#getTertiaryColor()}.
+ */
+public final class WallpaperColors implements Parcelable {
+
+ /**
+ * Specifies that dark text is preferred over the current wallpaper for best presentation.
+ * <p>
+ * eg. A launcher may set its text color to black if this flag is specified.
+ * @hide
+ */
+ public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0;
+
+ /**
+ * Specifies that dark theme is preferred over the current wallpaper for best presentation.
+ * <p>
+ * eg. A launcher may set its drawer color to black if this flag is specified.
+ * @hide
+ */
+ public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1;
+
+ /**
+ * Specifies that this object was generated by extracting colors from a bitmap.
+ * @hide
+ */
+ public static final int HINT_FROM_BITMAP = 1 << 2;
+
+ // Maximum size that a bitmap can have to keep our calculations sane
+ private static final int MAX_BITMAP_SIZE = 112;
+
+ // Even though we have a maximum size, we'll mainly match bitmap sizes
+ // using the area instead. This way our comparisons are aspect ratio independent.
+ private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE;
+
+ // When extracting the main colors, only consider colors
+ // present in at least MIN_COLOR_OCCURRENCE of the image
+ private static final float MIN_COLOR_OCCURRENCE = 0.05f;
+
+ // Decides when dark theme is optimal for this wallpaper
+ private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f;
+ // Minimum mean luminosity that an image needs to have to support dark text
+ private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f;
+ // We also check if the image has dark pixels in it,
+ // to avoid bright images with some dark spots.
+ private static final float DARK_PIXEL_LUMINANCE = 0.45f;
+ private static final float MAX_DARK_AREA = 0.05f;
+
+ private final ArrayList<Color> mMainColors;
+ private int mColorHints;
+
+ public WallpaperColors(Parcel parcel) {
+ mMainColors = new ArrayList<>();
+ final int count = parcel.readInt();
+ for (int i = 0; i < count; i++) {
+ final int colorInt = parcel.readInt();
+ Color color = Color.valueOf(colorInt);
+ mMainColors.add(color);
+ }
+ mColorHints = parcel.readInt();
+ }
+
+ /**
+ * Constructs {@link WallpaperColors} from a drawable.
+ * <p>
+ * Main colors will be extracted from the drawable.
+ *
+ * @param drawable Source where to extract from.
+ */
+ public static WallpaperColors fromDrawable(Drawable drawable) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+
+ // Some drawables do not have intrinsic dimensions
+ if (width <= 0 || height <= 0) {
+ width = MAX_BITMAP_SIZE;
+ height = MAX_BITMAP_SIZE;
+ }
+
+ Size optimalSize = calculateOptimalSize(width, height);
+ Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ final Canvas bmpCanvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ drawable.draw(bmpCanvas);
+
+ final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap);
+ bitmap.recycle();
+
+ return colors;
+ }
+
+ /**
+ * Constructs {@link WallpaperColors} from a bitmap.
+ * <p>
+ * Main colors will be extracted from the bitmap.
+ *
+ * @param bitmap Source where to extract from.
+ */
+ public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap can't be null");
+ }
+
+ final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
+ boolean shouldRecycle = false;
+ if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) {
+ shouldRecycle = true;
+ Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
+ bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
+ optimalSize.getHeight(), true /* filter */);
+ }
+
+ final Palette palette = Palette
+ .from(bitmap)
+ .setQuantizer(new VariationalKMeansQuantizer())
+ .maximumColorCount(5)
+ .clearFilters()
+ .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
+ .generate();
+
+ // Remove insignificant colors and sort swatches by population
+ final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
+ final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE;
+ swatches.removeIf(s -> s.getPopulation() < minColorArea);
+ swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
+
+ final int swatchesSize = swatches.size();
+ Color primary = null, secondary = null, tertiary = null;
+
+ swatchLoop:
+ for (int i = 0; i < swatchesSize; i++) {
+ Color color = Color.valueOf(swatches.get(i).getRgb());
+ switch (i) {
+ case 0:
+ primary = color;
+ break;
+ case 1:
+ secondary = color;
+ break;
+ case 2:
+ tertiary = color;
+ break;
+ default:
+ // out of bounds
+ break swatchLoop;
+ }
+ }
+
+ int hints = calculateDarkHints(bitmap);
+
+ if (shouldRecycle) {
+ bitmap.recycle();
+ }
+
+ return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints);
+ }
+
+ /**
+ * Constructs a new object from three colors.
+ *
+ * @param primaryColor Primary color.
+ * @param secondaryColor Secondary color.
+ * @param tertiaryColor Tertiary color.
+ * @see WallpaperColors#fromBitmap(Bitmap)
+ * @see WallpaperColors#fromDrawable(Drawable)
+ */
+ public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
+ @Nullable Color tertiaryColor) {
+ this(primaryColor, secondaryColor, tertiaryColor, 0);
+ }
+
+ /**
+ * Constructs a new object from three colors, where hints can be specified.
+ *
+ * @param primaryColor Primary color.
+ * @param secondaryColor Secondary color.
+ * @param tertiaryColor Tertiary color.
+ * @param colorHints A combination of WallpaperColor hints.
+ * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
+ * @see WallpaperColors#fromBitmap(Bitmap)
+ * @see WallpaperColors#fromDrawable(Drawable)
+ * @hide
+ */
+ public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
+ @Nullable Color tertiaryColor, int colorHints) {
+
+ if (primaryColor == null) {
+ throw new IllegalArgumentException("Primary color should never be null.");
+ }
+
+ mMainColors = new ArrayList<>(3);
+ mMainColors.add(primaryColor);
+ if (secondaryColor != null) {
+ mMainColors.add(secondaryColor);
+ }
+ if (tertiaryColor != null) {
+ if (secondaryColor == null) {
+ throw new IllegalArgumentException("tertiaryColor can't be specified when "
+ + "secondaryColor is null");
+ }
+ mMainColors.add(tertiaryColor);
+ }
+
+ mColorHints = colorHints;
+ }
+
+ public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
+ @Override
+ public WallpaperColors createFromParcel(Parcel in) {
+ return new WallpaperColors(in);
+ }
+
+ @Override
+ public WallpaperColors[] newArray(int size) {
+ return new WallpaperColors[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ List<Color> mainColors = getMainColors();
+ int count = mainColors.size();
+ dest.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ Color color = mainColors.get(i);
+ dest.writeInt(color.toArgb());
+ }
+ dest.writeInt(mColorHints);
+ }
+
+ /**
+ * Gets the most visually representative color of the wallpaper.
+ * "Visually representative" means easily noticeable in the image,
+ * probably happening at high frequency.
+ *
+ * @return A color.
+ */
+ public @NonNull Color getPrimaryColor() {
+ return mMainColors.get(0);
+ }
+
+ /**
+ * Gets the second most preeminent color of the wallpaper. Can be null.
+ *
+ * @return A color, may be null.
+ */
+ public @Nullable Color getSecondaryColor() {
+ return mMainColors.size() < 2 ? null : mMainColors.get(1);
+ }
+
+ /**
+ * Gets the third most preeminent color of the wallpaper. Can be null.
+ *
+ * @return A color, may be null.
+ */
+ public @Nullable Color getTertiaryColor() {
+ return mMainColors.size() < 3 ? null : mMainColors.get(2);
+ }
+
+ /**
+ * List of most preeminent colors, sorted by importance.
+ *
+ * @return List of colors.
+ * @hide
+ */
+ public @NonNull List<Color> getMainColors() {
+ return Collections.unmodifiableList(mMainColors);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ WallpaperColors other = (WallpaperColors) o;
+ return mMainColors.equals(other.mMainColors)
+ && mColorHints == other.mColorHints;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * mMainColors.hashCode() + mColorHints;
+ }
+
+ /**
+ * Combination of WallpaperColor hints.
+ *
+ * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
+ * @return True if dark text is supported.
+ * @hide
+ */
+ public int getColorHints() {
+ return mColorHints;
+ }
+
+ /**
+ * @param colorHints Combination of WallpaperColors hints.
+ * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
+ * @hide
+ */
+ public void setColorHints(int colorHints) {
+ mColorHints = colorHints;
+ }
+
+ /**
+ * Checks if image is bright and clean enough to support light text.
+ *
+ * @param source What to read.
+ * @return Whether image supports dark text or not.
+ */
+ private static int calculateDarkHints(Bitmap source) {
+ if (source == null) {
+ return 0;
+ }
+
+ int[] pixels = new int[source.getWidth() * source.getHeight()];
+ double totalLuminance = 0;
+ final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
+ int darkPixels = 0;
+ source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
+ source.getWidth(), source.getHeight());
+
+ // This bitmap was already resized to fit the maximum allowed area.
+ // Let's just loop through the pixels, no sweat!
+ float[] tmpHsl = new float[3];
+ for (int i = 0; i < pixels.length; i++) {
+ ColorUtils.colorToHSL(pixels[i], tmpHsl);
+ final float luminance = tmpHsl[2];
+ final int alpha = Color.alpha(pixels[i]);
+ // Make sure we don't have a dark pixel mass that will
+ // make text illegible.
+ if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
+ darkPixels++;
+ }
+ totalLuminance += luminance;
+ }
+
+ int hints = 0;
+ double meanLuminance = totalLuminance / pixels.length;
+ if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
+ hints |= HINT_SUPPORTS_DARK_TEXT;
+ }
+ if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
+ hints |= HINT_SUPPORTS_DARK_THEME;
+ }
+
+ return hints;
+ }
+
+ private static Size calculateOptimalSize(int width, int height) {
+ // Calculate how big the bitmap needs to be.
+ // This avoids unnecessary processing and allocation inside Palette.
+ final int requestedArea = width * height;
+ double scale = 1;
+ if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
+ scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
+ }
+ int newWidth = (int) (width * scale);
+ int newHeight = (int) (height * scale);
+ // Dealing with edge cases of the drawable being too wide or too tall.
+ // Width or height would end up being 0, in this case we'll set it to 1.
+ if (newWidth == 0) {
+ newWidth = 1;
+ }
+ if (newHeight == 0) {
+ newHeight = 1;
+ }
+
+ return new Size(newWidth, newHeight);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder colors = new StringBuilder();
+ for (int i = 0; i < mMainColors.size(); i++) {
+ colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" ");
+ }
+ return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]";
+ }
+}
diff --git a/android/app/WallpaperInfo.java b/android/app/WallpaperInfo.java
new file mode 100644
index 00000000..9d40381f
--- /dev/null
+++ b/android/app/WallpaperInfo.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.wallpaper.WallpaperService;
+import android.util.AttributeSet;
+import android.util.Printer;
+import android.util.Xml;
+
+import java.io.IOException;
+
+/**
+ * This class is used to specify meta information of a wallpaper service.
+ */
+public final class WallpaperInfo implements Parcelable {
+ static final String TAG = "WallpaperInfo";
+
+ /**
+ * The Service that implements this wallpaper component.
+ */
+ final ResolveInfo mService;
+
+ /**
+ * The wallpaper setting activity's name, to
+ * launch the setting activity of this wallpaper.
+ */
+ final String mSettingsActivityName;
+
+ /**
+ * Resource identifier for this wallpaper's thumbnail image.
+ */
+ final int mThumbnailResource;
+
+ /**
+ * Resource identifier for a string indicating the author of the wallpaper.
+ */
+ final int mAuthorResource;
+
+ /**
+ * Resource identifier for a string containing a short description of the wallpaper.
+ */
+ final int mDescriptionResource;
+
+ final int mContextUriResource;
+ final int mContextDescriptionResource;
+ final boolean mShowMetadataInPreview;
+
+ /**
+ * Constructor.
+ *
+ * @param context The Context in which we are parsing the wallpaper.
+ * @param service The ResolveInfo returned from the package manager about
+ * this wallpaper's component.
+ */
+ public WallpaperInfo(Context context, ResolveInfo service)
+ throws XmlPullParserException, IOException {
+ mService = service;
+ ServiceInfo si = service.serviceInfo;
+
+ PackageManager pm = context.getPackageManager();
+ String settingsActivityComponent = null;
+ int thumbnailRes = -1;
+ int authorRes = -1;
+ int descriptionRes = -1;
+ int contextUriRes = -1;
+ int contextDescriptionRes = -1;
+ boolean showMetadataInPreview = false;
+
+ XmlResourceParser parser = null;
+ try {
+ parser = si.loadXmlMetaData(pm, WallpaperService.SERVICE_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException("No "
+ + WallpaperService.SERVICE_META_DATA + " meta-data");
+ }
+
+ Resources res = pm.getResourcesForApplication(si.applicationInfo);
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"wallpaper".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with wallpaper tag");
+ }
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.Wallpaper);
+ settingsActivityComponent = sa.getString(
+ com.android.internal.R.styleable.Wallpaper_settingsActivity);
+
+ thumbnailRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_thumbnail,
+ -1);
+ authorRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_author,
+ -1);
+ descriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_description,
+ -1);
+ contextUriRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_contextUri,
+ -1);
+ contextDescriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.Wallpaper_contextDescription,
+ -1);
+ showMetadataInPreview = sa.getBoolean(
+ com.android.internal.R.styleable.Wallpaper_showMetadataInPreview,
+ false);
+
+ sa.recycle();
+ } catch (NameNotFoundException e) {
+ throw new XmlPullParserException(
+ "Unable to create context for: " + si.packageName);
+ } finally {
+ if (parser != null) parser.close();
+ }
+
+ mSettingsActivityName = settingsActivityComponent;
+ mThumbnailResource = thumbnailRes;
+ mAuthorResource = authorRes;
+ mDescriptionResource = descriptionRes;
+ mContextUriResource = contextUriRes;
+ mContextDescriptionResource = contextDescriptionRes;
+ mShowMetadataInPreview = showMetadataInPreview;
+ }
+
+ WallpaperInfo(Parcel source) {
+ mSettingsActivityName = source.readString();
+ mThumbnailResource = source.readInt();
+ mAuthorResource = source.readInt();
+ mDescriptionResource = source.readInt();
+ mContextUriResource = source.readInt();
+ mContextDescriptionResource = source.readInt();
+ mShowMetadataInPreview = source.readInt() != 0;
+ mService = ResolveInfo.CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * Return the .apk package that implements this wallpaper.
+ */
+ public String getPackageName() {
+ return mService.serviceInfo.packageName;
+ }
+
+ /**
+ * Return the class name of the service component that implements
+ * this wallpaper.
+ */
+ public String getServiceName() {
+ return mService.serviceInfo.name;
+ }
+
+ /**
+ * Return the raw information about the Service implementing this
+ * wallpaper. Do not modify the returned object.
+ */
+ public ServiceInfo getServiceInfo() {
+ return mService.serviceInfo;
+ }
+
+ /**
+ * Return the component of the service that implements this wallpaper.
+ */
+ public ComponentName getComponent() {
+ return new ComponentName(mService.serviceInfo.packageName,
+ mService.serviceInfo.name);
+ }
+
+ /**
+ * Load the user-displayed label for this wallpaper.
+ *
+ * @param pm Supply a PackageManager used to load the wallpaper's
+ * resources.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ return mService.loadLabel(pm);
+ }
+
+ /**
+ * Load the user-displayed icon for this wallpaper.
+ *
+ * @param pm Supply a PackageManager used to load the wallpaper's
+ * resources.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ return mService.loadIcon(pm);
+ }
+
+ /**
+ * Load the thumbnail image for this wallpaper.
+ *
+ * @param pm Supply a PackageManager used to load the wallpaper's
+ * resources.
+ */
+ public Drawable loadThumbnail(PackageManager pm) {
+ if (mThumbnailResource < 0) return null;
+
+ return pm.getDrawable(mService.serviceInfo.packageName,
+ mThumbnailResource,
+ mService.serviceInfo.applicationInfo);
+ }
+
+ /**
+ * Return a string indicating the author(s) of this wallpaper.
+ */
+ public CharSequence loadAuthor(PackageManager pm) throws NotFoundException {
+ if (mAuthorResource <= 0) throw new NotFoundException();
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ return pm.getText(packageName, mAuthorResource, applicationInfo);
+ }
+
+ /**
+ * Return a brief summary of this wallpaper's behavior.
+ */
+ public CharSequence loadDescription(PackageManager pm) throws NotFoundException {
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ if (mService.serviceInfo.descriptionRes != 0) {
+ return pm.getText(packageName, mService.serviceInfo.descriptionRes,
+ applicationInfo);
+
+ }
+ if (mDescriptionResource <= 0) throw new NotFoundException();
+ return pm.getText(packageName, mDescriptionResource,
+ mService.serviceInfo.applicationInfo);
+ }
+
+ /**
+ * Returns an URI that specifies a link for further context about this wallpaper.
+ *
+ * @param pm An instance of {@link PackageManager} to retrieve the URI.
+ * @return The URI.
+ */
+ public Uri loadContextUri(PackageManager pm) throws NotFoundException {
+ if (mContextUriResource <= 0) throw new NotFoundException();
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ String contextUriString = pm.getText(
+ packageName, mContextUriResource, applicationInfo).toString();
+ if (contextUriString == null) {
+ return null;
+ }
+ return Uri.parse(contextUriString);
+ }
+
+ /**
+ * Retrieves a title of the URI that specifies a link for further context about this wallpaper.
+ *
+ * @param pm An instance of {@link PackageManager} to retrieve the title.
+ * @return The title.
+ */
+ public CharSequence loadContextDescription(PackageManager pm) throws NotFoundException {
+ if (mContextDescriptionResource <= 0) throw new NotFoundException();
+ String packageName = mService.resolvePackageName;
+ ApplicationInfo applicationInfo = null;
+ if (packageName == null) {
+ packageName = mService.serviceInfo.packageName;
+ applicationInfo = mService.serviceInfo.applicationInfo;
+ }
+ return pm.getText(packageName, mContextDescriptionResource, applicationInfo).toString();
+ }
+
+ /**
+ * Queries whether any metadata should be shown when previewing the wallpaper. If this value is
+ * set to true, any component that shows a preview of this live wallpaper should also show
+ * accompanying information like {@link #loadLabel},
+ * {@link #loadDescription}, {@link #loadAuthor} and
+ * {@link #loadContextDescription(PackageManager)}, so the user gets to know further information
+ * about this wallpaper.
+ *
+ * @return Whether any metadata should be shown when previewing the wallpaper.
+ */
+ public boolean getShowMetadataInPreview() {
+ return mShowMetadataInPreview;
+ }
+
+ /**
+ * Return the class name of an activity that provides a settings UI for
+ * the wallpaper. You can launch this activity be starting it with
+ * an {@link android.content.Intent} whose action is MAIN and with an
+ * explicit {@link android.content.ComponentName}
+ * composed of {@link #getPackageName} and the class name returned here.
+ *
+ * <p>A null will be returned if there is no settings activity associated
+ * with the wallpaper.
+ */
+ public String getSettingsActivity() {
+ return mSettingsActivityName;
+ }
+
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "Service:");
+ mService.dump(pw, prefix + " ");
+ pw.println(prefix + "mSettingsActivityName=" + mSettingsActivityName);
+ }
+
+ @Override
+ public String toString() {
+ return "WallpaperInfo{" + mService.serviceInfo.name
+ + ", settings: "
+ + mSettingsActivityName + "}";
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mSettingsActivityName);
+ dest.writeInt(mThumbnailResource);
+ dest.writeInt(mAuthorResource);
+ dest.writeInt(mDescriptionResource);
+ dest.writeInt(mContextUriResource);
+ dest.writeInt(mContextDescriptionResource);
+ dest.writeInt(mShowMetadataInPreview ? 1 : 0);
+ mService.writeToParcel(dest, flags);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<WallpaperInfo> CREATOR = new Parcelable.Creator<WallpaperInfo>() {
+ public WallpaperInfo createFromParcel(Parcel source) {
+ return new WallpaperInfo(source);
+ }
+
+ public WallpaperInfo[] newArray(int size) {
+ return new WallpaperInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java
new file mode 100644
index 00000000..942cc995
--- /dev/null
+++ b/android/app/WallpaperManager.java
@@ -0,0 +1,1976 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RawRes;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadSystemException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.service.wallpaper.WallpaperService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.WindowManagerGlobal;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides access to the system wallpaper. With WallpaperManager, you can
+ * get the current wallpaper, get the desired dimensions for the wallpaper, set
+ * the wallpaper, and more.
+ *
+ * <p> An app can check whether wallpapers are supported for the current user, by calling
+ * {@link #isWallpaperSupported()}, and whether setting of wallpapers is allowed, by calling
+ * {@link #isSetWallpaperAllowed()}.
+ */
+@SystemService(Context.WALLPAPER_SERVICE)
+public class WallpaperManager {
+ private static String TAG = "WallpaperManager";
+ private static boolean DEBUG = false;
+ private float mWallpaperXStep = -1;
+ private float mWallpaperYStep = -1;
+
+ /** {@hide} */
+ private static final String PROP_WALLPAPER = "ro.config.wallpaper";
+ /** {@hide} */
+ private static final String PROP_LOCK_WALLPAPER = "ro.config.lock_wallpaper";
+ /** {@hide} */
+ private static final String PROP_WALLPAPER_COMPONENT = "ro.config.wallpaper_component";
+
+ /**
+ * Activity Action: Show settings for choosing wallpaper. Do not use directly to construct
+ * an intent; instead, use {@link #getCropAndSetWallpaperIntent}.
+ * <p>Input: {@link Intent#getData} is the URI of the image to crop and set as wallpaper.
+ * <p>Output: RESULT_OK if user decided to crop/set the wallpaper, RESULT_CANCEL otherwise
+ * Activities that support this intent should specify a MIME filter of "image/*"
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CROP_AND_SET_WALLPAPER =
+ "android.service.wallpaper.CROP_AND_SET_WALLPAPER";
+
+ /**
+ * Launch an activity for the user to pick the current global live
+ * wallpaper.
+ */
+ public static final String ACTION_LIVE_WALLPAPER_CHOOSER
+ = "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER";
+
+ /**
+ * Directly launch live wallpaper preview, allowing the user to immediately
+ * confirm to switch to a specific live wallpaper. You must specify
+ * {@link #EXTRA_LIVE_WALLPAPER_COMPONENT} with the ComponentName of
+ * a live wallpaper component that is to be shown.
+ */
+ public static final String ACTION_CHANGE_LIVE_WALLPAPER
+ = "android.service.wallpaper.CHANGE_LIVE_WALLPAPER";
+
+ /**
+ * Extra in {@link #ACTION_CHANGE_LIVE_WALLPAPER} that specifies the
+ * ComponentName of a live wallpaper that should be shown as a preview,
+ * for the user to confirm.
+ */
+ public static final String EXTRA_LIVE_WALLPAPER_COMPONENT
+ = "android.service.wallpaper.extra.LIVE_WALLPAPER_COMPONENT";
+
+ /**
+ * Manifest entry for activities that respond to {@link Intent#ACTION_SET_WALLPAPER}
+ * which allows them to provide a custom large icon associated with this action.
+ */
+ public static final String WALLPAPER_PREVIEW_META_DATA = "android.wallpaper.preview";
+
+ /**
+ * Command for {@link #sendWallpaperCommand}: reported by the wallpaper
+ * host when the user taps on an empty area (not performing an action
+ * in the host). The x and y arguments are the location of the tap in
+ * screen coordinates.
+ */
+ public static final String COMMAND_TAP = "android.wallpaper.tap";
+
+ /**
+ * Command for {@link #sendWallpaperCommand}: reported by the wallpaper
+ * host when the user releases a secondary pointer on an empty area
+ * (not performing an action in the host). The x and y arguments are
+ * the location of the secondary tap in screen coordinates.
+ */
+ public static final String COMMAND_SECONDARY_TAP = "android.wallpaper.secondaryTap";
+
+ /**
+ * Command for {@link #sendWallpaperCommand}: reported by the wallpaper
+ * host when the user drops an object into an area of the host. The x
+ * and y arguments are the location of the drop.
+ */
+ public static final String COMMAND_DROP = "android.home.drop";
+
+ /**
+ * Extra passed back from setWallpaper() giving the new wallpaper's assigned ID.
+ * @hide
+ */
+ public static final String EXTRA_NEW_WALLPAPER_ID = "android.service.wallpaper.extra.ID";
+
+ // flags for which kind of wallpaper to act on
+
+ /** @hide */
+ @IntDef(flag = true, value = {
+ FLAG_SYSTEM,
+ FLAG_LOCK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SetWallpaperFlags {}
+
+ /**
+ * Flag: set or retrieve the general system wallpaper.
+ */
+ public static final int FLAG_SYSTEM = 1 << 0;
+
+ /**
+ * Flag: set or retrieve the lock-screen-specific wallpaper.
+ */
+ public static final int FLAG_LOCK = 1 << 1;
+
+ private final Context mContext;
+
+ /**
+ * Special drawable that draws a wallpaper as fast as possible. Assumes
+ * no scaling or placement off (0,0) of the wallpaper (this should be done
+ * at the time the bitmap is loaded).
+ */
+ static class FastBitmapDrawable extends Drawable {
+ private final Bitmap mBitmap;
+ private final int mWidth;
+ private final int mHeight;
+ private int mDrawLeft;
+ private int mDrawTop;
+ private final Paint mPaint;
+
+ private FastBitmapDrawable(Bitmap bitmap) {
+ mBitmap = bitmap;
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+
+ setBounds(0, 0, mWidth, mHeight);
+
+ mPaint = new Paint();
+ mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, mDrawLeft, mDrawTop, mPaint);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void setBounds(int left, int top, int right, int bottom) {
+ mDrawLeft = left + (right-left - mWidth) / 2;
+ mDrawTop = top + (bottom-top - mHeight) / 2;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ throw new UnsupportedOperationException("Not supported with this drawable");
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ throw new UnsupportedOperationException("Not supported with this drawable");
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ throw new UnsupportedOperationException("Not supported with this drawable");
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filter) {
+ throw new UnsupportedOperationException("Not supported with this drawable");
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return mHeight;
+ }
+ }
+
+ private static class Globals extends IWallpaperManagerCallback.Stub {
+ private final IWallpaperManager mService;
+ private boolean mColorCallbackRegistered;
+ private final ArrayList<Pair<OnColorsChangedListener, Handler>> mColorListeners =
+ new ArrayList<>();
+ private Bitmap mCachedWallpaper;
+ private int mCachedWallpaperUserId;
+ private Bitmap mDefaultWallpaper;
+ private Handler mMainLooperHandler;
+
+ Globals(Looper looper) {
+ IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE);
+ mService = IWallpaperManager.Stub.asInterface(b);
+ mMainLooperHandler = new Handler(looper);
+ forgetLoadedWallpaper();
+ }
+
+ public void onWallpaperChanged() {
+ /* The wallpaper has changed but we shouldn't eagerly load the
+ * wallpaper as that would be inefficient. Reset the cached wallpaper
+ * to null so if the user requests the wallpaper again then we'll
+ * fetch it.
+ */
+ forgetLoadedWallpaper();
+ }
+
+ /**
+ * Start listening to wallpaper color events.
+ * Will be called whenever someone changes their wallpaper or if a live wallpaper
+ * changes its colors.
+ * @param callback Listener
+ * @param handler Thread to call it from. Main thread if null.
+ * @param userId Owner of the wallpaper or UserHandle.USER_ALL
+ */
+ public void addOnColorsChangedListener(@NonNull OnColorsChangedListener callback,
+ @Nullable Handler handler, int userId) {
+ synchronized (this) {
+ if (!mColorCallbackRegistered) {
+ try {
+ mService.registerWallpaperColorsCallback(this, userId);
+ mColorCallbackRegistered = true;
+ } catch (RemoteException e) {
+ // Failed, service is gone
+ Log.w(TAG, "Can't register for color updates", e);
+ }
+ }
+ mColorListeners.add(new Pair<>(callback, handler));
+ }
+ }
+
+ /**
+ * Stop listening to wallpaper color events.
+ *
+ * @param callback listener
+ * @param userId Owner of the wallpaper or UserHandle.USER_ALL
+ */
+ public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener callback,
+ int userId) {
+ synchronized (this) {
+ mColorListeners.removeIf(pair -> pair.first == callback);
+
+ if (mColorListeners.size() == 0 && mColorCallbackRegistered) {
+ mColorCallbackRegistered = false;
+ try {
+ mService.unregisterWallpaperColorsCallback(this, userId);
+ } catch (RemoteException e) {
+ // Failed, service is gone
+ Log.w(TAG, "Can't unregister color updates", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onWallpaperColorsChanged(WallpaperColors colors, int which, int userId) {
+ synchronized (this) {
+ for (Pair<OnColorsChangedListener, Handler> listener : mColorListeners) {
+ Handler handler = listener.second;
+ if (listener.second == null) {
+ handler = mMainLooperHandler;
+ }
+ handler.post(() -> {
+ // Dealing with race conditions between posting a callback and
+ // removeOnColorsChangedListener being called.
+ boolean stillExists;
+ synchronized (sGlobals) {
+ stillExists = mColorListeners.contains(listener);
+ }
+ if (stillExists) {
+ listener.first.onColorsChanged(colors, which, userId);
+ }
+ });
+ }
+ }
+ }
+
+ WallpaperColors getWallpaperColors(int which, int userId) {
+ if (which != FLAG_LOCK && which != FLAG_SYSTEM) {
+ throw new IllegalArgumentException(
+ "Must request colors for exactly one kind of wallpaper");
+ }
+
+ try {
+ return mService.getWallpaperColors(which, userId);
+ } catch (RemoteException e) {
+ // Can't get colors, connection lost.
+ }
+ return null;
+ }
+
+ public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault,
+ @SetWallpaperFlags int which) {
+ return peekWallpaperBitmap(context, returnDefault, which, context.getUserId());
+ }
+
+ public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault,
+ @SetWallpaperFlags int which, int userId) {
+ if (mService != null) {
+ try {
+ if (!mService.isWallpaperSupported(context.getOpPackageName())) {
+ return null;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ synchronized (this) {
+ if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) {
+ return mCachedWallpaper;
+ }
+ mCachedWallpaper = null;
+ mCachedWallpaperUserId = 0;
+ try {
+ mCachedWallpaper = getCurrentWallpaperLocked(context, userId);
+ mCachedWallpaperUserId = userId;
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Out of memory loading the current wallpaper: " + e);
+ } catch (SecurityException e) {
+ if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+ Log.w(TAG, "No permission to access wallpaper, suppressing"
+ + " exception to avoid crashing legacy app.");
+ } else {
+ // Post-O apps really most sincerely need the permission.
+ throw e;
+ }
+ }
+ if (mCachedWallpaper != null) {
+ return mCachedWallpaper;
+ }
+ }
+ if (returnDefault) {
+ Bitmap defaultWallpaper = mDefaultWallpaper;
+ if (defaultWallpaper == null) {
+ defaultWallpaper = getDefaultWallpaper(context, which);
+ synchronized (this) {
+ mDefaultWallpaper = defaultWallpaper;
+ }
+ }
+ return defaultWallpaper;
+ }
+ return null;
+ }
+
+ void forgetLoadedWallpaper() {
+ synchronized (this) {
+ mCachedWallpaper = null;
+ mCachedWallpaperUserId = 0;
+ mDefaultWallpaper = null;
+ }
+ }
+
+ private Bitmap getCurrentWallpaperLocked(Context context, int userId) {
+ if (mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ return null;
+ }
+
+ try {
+ Bundle params = new Bundle();
+ ParcelFileDescriptor fd = mService.getWallpaper(context.getOpPackageName(),
+ this, FLAG_SYSTEM, params, userId);
+ if (fd != null) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ return BitmapFactory.decodeFileDescriptor(
+ fd.getFileDescriptor(), null, options);
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Can't decode file", e);
+ } finally {
+ IoUtils.closeQuietly(fd);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return null;
+ }
+
+ private Bitmap getDefaultWallpaper(Context context, @SetWallpaperFlags int which) {
+ InputStream is = openDefaultWallpaper(context, which);
+ if (is != null) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ return BitmapFactory.decodeStream(is, null, options);
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Can't decode stream", e);
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
+ return null;
+ }
+ }
+
+ private static final Object sSync = new Object[0];
+ private static Globals sGlobals;
+
+ static void initGlobals(Looper looper) {
+ synchronized (sSync) {
+ if (sGlobals == null) {
+ sGlobals = new Globals(looper);
+ }
+ }
+ }
+
+ /*package*/ WallpaperManager(Context context, Handler handler) {
+ mContext = context;
+ initGlobals(context.getMainLooper());
+ }
+
+ /**
+ * Retrieve a WallpaperManager associated with the given Context.
+ */
+ public static WallpaperManager getInstance(Context context) {
+ return (WallpaperManager)context.getSystemService(
+ Context.WALLPAPER_SERVICE);
+ }
+
+ /** @hide */
+ public IWallpaperManager getIWallpaperManager() {
+ return sGlobals.mService;
+ }
+
+ /**
+ * Retrieve the current system wallpaper; if
+ * no wallpaper is set, the system built-in static wallpaper is returned.
+ * This is returned as an
+ * abstract Drawable that you can install in a View to display whatever
+ * wallpaper the user has currently set.
+ * <p>
+ * This method can return null if there is no system wallpaper available, if
+ * wallpapers are not supported in the current user, or if the calling app is not
+ * permitted to access the system wallpaper.
+ *
+ * @return Returns a Drawable object that will draw the system wallpaper,
+ * or {@code null} if no system wallpaper exists or if the calling application
+ * is not able to access the wallpaper.
+ */
+ public Drawable getDrawable() {
+ Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM);
+ if (bm != null) {
+ Drawable dr = new BitmapDrawable(mContext.getResources(), bm);
+ dr.setDither(false);
+ return dr;
+ }
+ return null;
+ }
+
+ /**
+ * Obtain a drawable for the built-in static system wallpaper.
+ */
+ public Drawable getBuiltInDrawable() {
+ return getBuiltInDrawable(0, 0, false, 0, 0, FLAG_SYSTEM);
+ }
+
+ /**
+ * Obtain a drawable for the specified built-in static system wallpaper.
+ *
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return A Drawable presenting the specified wallpaper image, or {@code null}
+ * if no built-in default image for that wallpaper type exists.
+ */
+ public Drawable getBuiltInDrawable(@SetWallpaperFlags int which) {
+ return getBuiltInDrawable(0, 0, false, 0, 0, which);
+ }
+
+ /**
+ * Returns a drawable for the system built-in static wallpaper. Based on the parameters, the
+ * drawable can be cropped and scaled
+ *
+ * @param outWidth The width of the returned drawable
+ * @param outWidth The height of the returned drawable
+ * @param scaleToFit If true, scale the wallpaper down rather than just cropping it
+ * @param horizontalAlignment A float value between 0 and 1 specifying where to crop the image;
+ * 0 for left-aligned, 0.5 for horizontal center-aligned, and 1 for right-aligned
+ * @param verticalAlignment A float value between 0 and 1 specifying where to crop the image;
+ * 0 for top-aligned, 0.5 for vertical center-aligned, and 1 for bottom-aligned
+ * @return A Drawable presenting the built-in default system wallpaper image,
+ * or {@code null} if no such default image is defined on this device.
+ */
+ public Drawable getBuiltInDrawable(int outWidth, int outHeight,
+ boolean scaleToFit, float horizontalAlignment, float verticalAlignment) {
+ return getBuiltInDrawable(outWidth, outHeight, scaleToFit,
+ horizontalAlignment, verticalAlignment, FLAG_SYSTEM);
+ }
+
+ /**
+ * Returns a drawable for the built-in static wallpaper of the specified type. Based on the
+ * parameters, the drawable can be cropped and scaled.
+ *
+ * @param outWidth The width of the returned drawable
+ * @param outWidth The height of the returned drawable
+ * @param scaleToFit If true, scale the wallpaper down rather than just cropping it
+ * @param horizontalAlignment A float value between 0 and 1 specifying where to crop the image;
+ * 0 for left-aligned, 0.5 for horizontal center-aligned, and 1 for right-aligned
+ * @param verticalAlignment A float value between 0 and 1 specifying where to crop the image;
+ * 0 for top-aligned, 0.5 for vertical center-aligned, and 1 for bottom-aligned
+ * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws
+ * IllegalArgumentException if an invalid wallpaper is requested.
+ * @return A Drawable presenting the built-in default wallpaper image of the given type,
+ * or {@code null} if no default image of that type is defined on this device.
+ */
+ public Drawable getBuiltInDrawable(int outWidth, int outHeight, boolean scaleToFit,
+ float horizontalAlignment, float verticalAlignment, @SetWallpaperFlags int which) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+
+ if (which != FLAG_SYSTEM && which != FLAG_LOCK) {
+ throw new IllegalArgumentException("Must request exactly one kind of wallpaper");
+ }
+
+ Resources resources = mContext.getResources();
+ horizontalAlignment = Math.max(0, Math.min(1, horizontalAlignment));
+ verticalAlignment = Math.max(0, Math.min(1, verticalAlignment));
+
+ InputStream wpStream = openDefaultWallpaper(mContext, which);
+ if (wpStream == null) {
+ if (DEBUG) {
+ Log.w(TAG, "default wallpaper stream " + which + " is null");
+ }
+ return null;
+ } else {
+ InputStream is = new BufferedInputStream(wpStream);
+ if (outWidth <= 0 || outHeight <= 0) {
+ Bitmap fullSize = BitmapFactory.decodeStream(is, null, null);
+ return new BitmapDrawable(resources, fullSize);
+ } else {
+ int inWidth;
+ int inHeight;
+ // Just measure this time through...
+ {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, options);
+ if (options.outWidth != 0 && options.outHeight != 0) {
+ inWidth = options.outWidth;
+ inHeight = options.outHeight;
+ } else {
+ Log.e(TAG, "default wallpaper dimensions are 0");
+ return null;
+ }
+ }
+
+ // Reopen the stream to do the full decode. We know at this point
+ // that openDefaultWallpaper() will return non-null.
+ is = new BufferedInputStream(openDefaultWallpaper(mContext, which));
+
+ RectF cropRectF;
+
+ outWidth = Math.min(inWidth, outWidth);
+ outHeight = Math.min(inHeight, outHeight);
+ if (scaleToFit) {
+ cropRectF = getMaxCropRect(inWidth, inHeight, outWidth, outHeight,
+ horizontalAlignment, verticalAlignment);
+ } else {
+ float left = (inWidth - outWidth) * horizontalAlignment;
+ float right = left + outWidth;
+ float top = (inHeight - outHeight) * verticalAlignment;
+ float bottom = top + outHeight;
+ cropRectF = new RectF(left, top, right, bottom);
+ }
+ Rect roundedTrueCrop = new Rect();
+ cropRectF.roundOut(roundedTrueCrop);
+
+ if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+ Log.w(TAG, "crop has bad values for full size image");
+ return null;
+ }
+
+ // See how much we're reducing the size of the image
+ int scaleDownSampleSize = Math.min(roundedTrueCrop.width() / outWidth,
+ roundedTrueCrop.height() / outHeight);
+
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(is, true);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot open region decoder for default wallpaper");
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
+
+ if (crop == null) {
+ // BitmapRegionDecoder has failed, try to crop in-memory. We know at
+ // this point that openDefaultWallpaper() will return non-null.
+ is = new BufferedInputStream(openDefaultWallpaper(mContext, which));
+ Bitmap fullSize = null;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ fullSize = BitmapFactory.decodeStream(is, null, options);
+ if (fullSize != null) {
+ crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+ roundedTrueCrop.top, roundedTrueCrop.width(),
+ roundedTrueCrop.height());
+ }
+ }
+
+ if (crop == null) {
+ Log.w(TAG, "cannot decode default wallpaper");
+ return null;
+ }
+
+ // Scale down if necessary
+ if (outWidth > 0 && outHeight > 0 &&
+ (crop.getWidth() != outWidth || crop.getHeight() != outHeight)) {
+ Matrix m = new Matrix();
+ RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+ RectF returnRect = new RectF(0, 0, outWidth, outHeight);
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ Paint p = new Paint();
+ p.setFilterBitmap(true);
+ c.drawBitmap(crop, m, p);
+ crop = tmp;
+ }
+ }
+
+ return new BitmapDrawable(resources, crop);
+ }
+ }
+ }
+
+ private static RectF getMaxCropRect(int inWidth, int inHeight, int outWidth, int outHeight,
+ float horizontalAlignment, float verticalAlignment) {
+ RectF cropRect = new RectF();
+ // Get a crop rect that will fit this
+ if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
+ cropRect.top = 0;
+ cropRect.bottom = inHeight;
+ float cropWidth = outWidth * (inHeight / (float) outHeight);
+ cropRect.left = (inWidth - cropWidth) * horizontalAlignment;
+ cropRect.right = cropRect.left + cropWidth;
+ } else {
+ cropRect.left = 0;
+ cropRect.right = inWidth;
+ float cropHeight = outHeight * (inWidth / (float) outWidth);
+ cropRect.top = (inHeight - cropHeight) * verticalAlignment;
+ cropRect.bottom = cropRect.top + cropHeight;
+ }
+ return cropRect;
+ }
+
+ /**
+ * Retrieve the current system wallpaper; if there is no wallpaper set,
+ * a null pointer is returned. This is returned as an
+ * abstract Drawable that you can install in a View to display whatever
+ * wallpaper the user has currently set.
+ *
+ * @return Returns a Drawable object that will draw the wallpaper or a
+ * null pointer if these is none.
+ */
+ public Drawable peekDrawable() {
+ Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false, FLAG_SYSTEM);
+ if (bm != null) {
+ Drawable dr = new BitmapDrawable(mContext.getResources(), bm);
+ dr.setDither(false);
+ return dr;
+ }
+ return null;
+ }
+
+ /**
+ * Like {@link #getDrawable()}, but the returned Drawable has a number
+ * of limitations to reduce its overhead as much as possible. It will
+ * never scale the wallpaper (only centering it if the requested bounds
+ * do match the bitmap bounds, which should not be typical), doesn't
+ * allow setting an alpha, color filter, or other attributes, etc. The
+ * bounds of the returned drawable will be initialized to the same bounds
+ * as the wallpaper, so normally you will not need to touch it. The
+ * drawable also assumes that it will be used in a context running in
+ * the same density as the screen (not in density compatibility mode).
+ *
+ * @return Returns a Drawable object that will draw the wallpaper.
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ public Drawable getFastDrawable() {
+ Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM);
+ if (bm != null) {
+ return new FastBitmapDrawable(bm);
+ }
+ return null;
+ }
+
+ /**
+ * Like {@link #getFastDrawable()}, but if there is no wallpaper set,
+ * a null pointer is returned.
+ *
+ * @return Returns an optimized Drawable object that will draw the
+ * wallpaper or a null pointer if these is none.
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ public Drawable peekFastDrawable() {
+ Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false, FLAG_SYSTEM);
+ if (bm != null) {
+ return new FastBitmapDrawable(bm);
+ }
+ return null;
+ }
+
+ /**
+ * Like {@link #getDrawable()} but returns a Bitmap.
+ *
+ * @hide
+ */
+ public Bitmap getBitmap() {
+ return getBitmapAsUser(mContext.getUserId());
+ }
+
+ /**
+ * Like {@link #getDrawable()} but returns a Bitmap for the provided user.
+ *
+ * @hide
+ */
+ public Bitmap getBitmapAsUser(int userId) {
+ return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId);
+ }
+
+ /**
+ * Get an open, readable file descriptor to the given wallpaper image file.
+ * The caller is responsible for closing the file descriptor when done ingesting the file.
+ *
+ * <p>If no lock-specific wallpaper has been configured for the given user, then
+ * this method will return {@code null} when requesting {@link #FLAG_LOCK} rather than
+ * returning the system wallpaper's image file.
+ *
+ * @param which The wallpaper whose image file is to be retrieved. Must be a single
+ * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}.
+ * @return An open, readable file desriptor to the requested wallpaper image file;
+ * or {@code null} if no such wallpaper is configured or if the calling app does
+ * not have permission to read the current wallpaper.
+ *
+ * @see #FLAG_LOCK
+ * @see #FLAG_SYSTEM
+ */
+ @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
+ public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which) {
+ return getWallpaperFile(which, mContext.getUserId());
+ }
+
+ /**
+ * Registers a listener to get notified when the wallpaper colors change.
+ * @param listener A listener to register
+ * @param handler Where to call it from. Will be called from the main thread
+ * if null.
+ */
+ public void addOnColorsChangedListener(@NonNull OnColorsChangedListener listener,
+ @NonNull Handler handler) {
+ addOnColorsChangedListener(listener, handler, mContext.getUserId());
+ }
+
+ /**
+ * Registers a listener to get notified when the wallpaper colors change
+ * @param listener A listener to register
+ * @param handler Where to call it from. Will be called from the main thread
+ * if null.
+ * @param userId Owner of the wallpaper or UserHandle.USER_ALL.
+ * @hide
+ */
+ public void addOnColorsChangedListener(@NonNull OnColorsChangedListener listener,
+ @NonNull Handler handler, int userId) {
+ sGlobals.addOnColorsChangedListener(listener, handler, userId);
+ }
+
+ /**
+ * Stop listening to color updates.
+ * @param callback A callback to unsubscribe.
+ */
+ public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener callback) {
+ removeOnColorsChangedListener(callback, mContext.getUserId());
+ }
+
+ /**
+ * Stop listening to color updates.
+ * @param callback A callback to unsubscribe.
+ * @param userId Owner of the wallpaper or UserHandle.USER_ALL.
+ * @hide
+ */
+ public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener callback,
+ int userId) {
+ sGlobals.removeOnColorsChangedListener(callback, userId);
+ }
+
+ /**
+ * Get the primary colors of a wallpaper.
+ *
+ * <p>You can expect null if:
+ * • Colors are still being processed by the system.
+ * • A live wallpaper doesn't implement {@link WallpaperService.Engine#onComputeColors()}.
+ *
+ * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}.
+ * @return Current {@link WallpaperColors} or null if colors are unknown.
+ * @see #addOnColorsChangedListener(OnColorsChangedListener, Handler)
+ */
+ public @Nullable WallpaperColors getWallpaperColors(int which) {
+ return getWallpaperColors(which, mContext.getUserId());
+ }
+
+ /**
+ * Get the primary colors of a wallpaper
+ * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}
+ * @param userId Owner of the wallpaper.
+ * @return {@link WallpaperColors} or null if colors are unknown.
+ * @hide
+ */
+ public @Nullable WallpaperColors getWallpaperColors(int which, int userId) {
+ return sGlobals.getWallpaperColors(which, userId);
+ }
+
+ /**
+ * Version of {@link #getWallpaperFile(int)} that can access the wallpaper data
+ * for a given user. The caller must hold the INTERACT_ACROSS_USERS_FULL
+ * permission to access another user's wallpaper data.
+ *
+ * @param which The wallpaper whose image file is to be retrieved. Must be a single
+ * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}.
+ * @param userId The user or profile whose imagery is to be retrieved
+ *
+ * @see #FLAG_LOCK
+ * @see #FLAG_SYSTEM
+ *
+ * @hide
+ */
+ public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which, int userId) {
+ if (which != FLAG_SYSTEM && which != FLAG_LOCK) {
+ throw new IllegalArgumentException("Must request exactly one kind of wallpaper");
+ }
+
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ try {
+ Bundle outParams = new Bundle();
+ return sGlobals.mService.getWallpaper(mContext.getOpPackageName(), null, which,
+ outParams, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SecurityException e) {
+ if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+ Log.w(TAG, "No permission to access wallpaper, suppressing"
+ + " exception to avoid crashing legacy app.");
+ return null;
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove all internal references to the last loaded wallpaper. Useful
+ * for apps that want to reduce memory usage when they only temporarily
+ * need to have the wallpaper. After calling, the next request for the
+ * wallpaper will require reloading it again from disk.
+ */
+ public void forgetLoadedWallpaper() {
+ sGlobals.forgetLoadedWallpaper();
+ }
+
+ /**
+ * If the current wallpaper is a live wallpaper component, return the
+ * information about that wallpaper. Otherwise, if it is a static image,
+ * simply return null.
+ */
+ public WallpaperInfo getWallpaperInfo() {
+ try {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ return sGlobals.mService.getWallpaperInfo(UserHandle.myUserId());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the ID of the current wallpaper of the given kind. If there is no
+ * such wallpaper configured, returns a negative number.
+ *
+ * <p>Every time the wallpaper image is set, a new ID is assigned to it.
+ * This method allows the caller to determine whether the wallpaper imagery
+ * has changed, regardless of how that change happened.
+ *
+ * @param which The wallpaper whose ID is to be returned. Must be a single
+ * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}.
+ * @return The positive numeric ID of the current wallpaper of the given kind,
+ * or a negative value if no such wallpaper is configured.
+ */
+ public int getWallpaperId(@SetWallpaperFlags int which) {
+ return getWallpaperIdForUser(which, mContext.getUserId());
+ }
+
+ /**
+ * Get the ID of the given user's current wallpaper of the given kind. If there
+ * is no such wallpaper configured, returns a negative number.
+ * @hide
+ */
+ public int getWallpaperIdForUser(@SetWallpaperFlags int which, int userId) {
+ try {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ return sGlobals.mService.getWallpaperIdForUser(which, userId);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets an Intent that will launch an activity that crops the given
+ * image and sets the device's wallpaper. If there is a default HOME activity
+ * that supports cropping wallpapers, it will be preferred as the default.
+ * Use this method instead of directly creating a {@link #ACTION_CROP_AND_SET_WALLPAPER}
+ * intent.
+ *
+ * @param imageUri The image URI that will be set in the intent. The must be a content
+ * URI and its provider must resolve its type to "image/*"
+ *
+ * @throws IllegalArgumentException if the URI is not a content URI or its MIME type is
+ * not "image/*"
+ */
+ public Intent getCropAndSetWallpaperIntent(Uri imageUri) {
+ if (imageUri == null) {
+ throw new IllegalArgumentException("Image URI must not be null");
+ }
+
+ if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme())) {
+ throw new IllegalArgumentException("Image URI must be of the "
+ + ContentResolver.SCHEME_CONTENT + " scheme type");
+ }
+
+ final PackageManager packageManager = mContext.getPackageManager();
+ Intent cropAndSetWallpaperIntent =
+ new Intent(ACTION_CROP_AND_SET_WALLPAPER, imageUri);
+ cropAndSetWallpaperIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ // Find out if the default HOME activity supports CROP_AND_SET_WALLPAPER
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
+ ResolveInfo resolvedHome = packageManager.resolveActivity(homeIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolvedHome != null) {
+ cropAndSetWallpaperIntent.setPackage(resolvedHome.activityInfo.packageName);
+
+ List<ResolveInfo> cropAppList = packageManager.queryIntentActivities(
+ cropAndSetWallpaperIntent, 0);
+ if (cropAppList.size() > 0) {
+ return cropAndSetWallpaperIntent;
+ }
+ }
+
+ // fallback crop activity
+ final String cropperPackage = mContext.getString(
+ com.android.internal.R.string.config_wallpaperCropperPackage);
+ cropAndSetWallpaperIntent.setPackage(cropperPackage);
+ List<ResolveInfo> cropAppList = packageManager.queryIntentActivities(
+ cropAndSetWallpaperIntent, 0);
+ if (cropAppList.size() > 0) {
+ return cropAndSetWallpaperIntent;
+ }
+ // If the URI is not of the right type, or for some reason the system wallpaper
+ // cropper doesn't exist, return null
+ throw new IllegalArgumentException("Cannot use passed URI to set wallpaper; " +
+ "check that the type returned by ContentProvider matches image/*");
+ }
+
+ /**
+ * Change the current system wallpaper to the bitmap in the given resource.
+ * The resource is opened as a raw data stream and copied into the
+ * wallpaper; it must be a valid PNG or JPEG image. On success, the intent
+ * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @param resid The resource ID of the bitmap to be used as the wallpaper image
+ *
+ * @throws IOException If an error occurs reverting to the built-in
+ * wallpaper.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void setResource(@RawRes int resid) throws IOException {
+ setResource(resid, FLAG_SYSTEM | FLAG_LOCK);
+ }
+
+ /**
+ * Version of {@link #setResource(int)} that allows the caller to specify which
+ * of the supported wallpaper categories to set.
+ *
+ * @param resid The resource ID of the bitmap to be used as the wallpaper image
+ * @param which Flags indicating which wallpaper(s) to configure with the new imagery
+ *
+ * @see #FLAG_LOCK
+ * @see #FLAG_SYSTEM
+ *
+ * @return An integer ID assigned to the newly active wallpaper; or zero on failure.
+ *
+ * @throws IOException
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setResource(@RawRes int resid, @SetWallpaperFlags int which)
+ throws IOException {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ final Bundle result = new Bundle();
+ final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ try {
+ Resources resources = mContext.getResources();
+ /* Set the wallpaper to the default values */
+ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(
+ "res:" + resources.getResourceName(resid),
+ mContext.getOpPackageName(), null, false, result, which, completion,
+ UserHandle.myUserId());
+ if (fd != null) {
+ FileOutputStream fos = null;
+ boolean ok = false;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ copyStreamToWallpaperFile(resources.openRawResource(resid), fos);
+ // The 'close()' is the trigger for any server-side image manipulation,
+ // so we must do that before waiting for completion.
+ fos.close();
+ completion.waitForCompletion();
+ } finally {
+ // Might be redundant but completion shouldn't wait unless the write
+ // succeeded; this is a fallback if it threw past the close+wait.
+ IoUtils.closeQuietly(fos);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);
+ }
+
+ /**
+ * Change the current system wallpaper to a bitmap. The given bitmap is
+ * converted to a PNG and stored as the wallpaper. On success, the intent
+ * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
+ *
+ * <p>This method is equivalent to calling
+ * {@link #setBitmap(Bitmap, Rect, boolean)} and passing {@code null} for the
+ * {@code visibleCrop} rectangle and {@code true} for the {@code allowBackup}
+ * parameter.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @param bitmap The bitmap to be used as the new system wallpaper.
+ *
+ * @throws IOException If an error occurs when attempting to set the wallpaper
+ * to the provided image.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void setBitmap(Bitmap bitmap) throws IOException {
+ setBitmap(bitmap, null, true);
+ }
+
+ /**
+ * Change the current system wallpaper to a bitmap, specifying a hint about
+ * which subrectangle of the full image is to be visible. The OS will then
+ * try to best present the given portion of the full image as the static system
+ * wallpaper image. On success, the intent
+ * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
+ *
+ * <p>Passing {@code null} as the {@code visibleHint} parameter is equivalent to
+ * passing (0, 0, {@code fullImage.getWidth()}, {@code fullImage.getHeight()}).
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @param fullImage A bitmap that will supply the wallpaper imagery.
+ * @param visibleCropHint The rectangular subregion of {@code fullImage} that should be
+ * displayed as wallpaper. Passing {@code null} for this parameter means that
+ * the full image should be displayed if possible given the image's and device's
+ * aspect ratios, etc.
+ * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper
+ * image for restore to a future device; {@code false} otherwise.
+ *
+ * @return An integer ID assigned to the newly active wallpaper; or zero on failure.
+ *
+ * @throws IOException If an error occurs when attempting to set the wallpaper
+ * to the provided image.
+ * @throws IllegalArgumentException If the {@code visibleCropHint} rectangle is
+ * empty or invalid.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup)
+ throws IOException {
+ return setBitmap(fullImage, visibleCropHint, allowBackup, FLAG_SYSTEM | FLAG_LOCK);
+ }
+
+ /**
+ * Version of {@link #setBitmap(Bitmap, Rect, boolean)} that allows the caller
+ * to specify which of the supported wallpaper categories to set.
+ *
+ * @param fullImage A bitmap that will supply the wallpaper imagery.
+ * @param visibleCropHint The rectangular subregion of {@code fullImage} that should be
+ * displayed as wallpaper. Passing {@code null} for this parameter means that
+ * the full image should be displayed if possible given the image's and device's
+ * aspect ratios, etc.
+ * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper
+ * image for restore to a future device; {@code false} otherwise.
+ * @param which Flags indicating which wallpaper(s) to configure with the new imagery.
+ *
+ * @see #FLAG_LOCK
+ * @see #FLAG_SYSTEM
+ *
+ * @return An integer ID assigned to the newly active wallpaper; or zero on failure.
+ *
+ * @throws IOException
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setBitmap(Bitmap fullImage, Rect visibleCropHint,
+ boolean allowBackup, @SetWallpaperFlags int which)
+ throws IOException {
+ return setBitmap(fullImage, visibleCropHint, allowBackup, which,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Like {@link #setBitmap(Bitmap, Rect, boolean, int)}, but allows to pass in an explicit user
+ * id. If the user id doesn't match the user id the process is running under, calling this
+ * requires permission {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
+ * @hide
+ */
+ public int setBitmap(Bitmap fullImage, Rect visibleCropHint,
+ boolean allowBackup, @SetWallpaperFlags int which, int userId)
+ throws IOException {
+ validateRect(visibleCropHint);
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ final Bundle result = new Bundle();
+ final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ try {
+ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
+ mContext.getOpPackageName(), visibleCropHint, allowBackup,
+ result, which, completion, userId);
+ if (fd != null) {
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos);
+ fos.close();
+ completion.waitForCompletion();
+ } finally {
+ IoUtils.closeQuietly(fos);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);
+ }
+
+ private final void validateRect(Rect rect) {
+ if (rect != null && rect.isEmpty()) {
+ throw new IllegalArgumentException("visibleCrop rectangle must be valid and non-empty");
+ }
+ }
+
+ /**
+ * Change the current system wallpaper to a specific byte stream. The
+ * give InputStream is copied into persistent storage and will now be
+ * used as the wallpaper. Currently it must be either a JPEG or PNG
+ * image. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
+ * is broadcast.
+ *
+ * <p>This method is equivalent to calling
+ * {@link #setStream(InputStream, Rect, boolean)} and passing {@code null} for the
+ * {@code visibleCrop} rectangle and {@code true} for the {@code allowBackup}
+ * parameter.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @param bitmapData A stream containing the raw data to install as a wallpaper. This
+ * data can be in any format handled by {@link BitmapRegionDecoder}.
+ *
+ * @throws IOException If an error occurs when attempting to set the wallpaper
+ * based on the provided image data.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void setStream(InputStream bitmapData) throws IOException {
+ setStream(bitmapData, null, true);
+ }
+
+ private void copyStreamToWallpaperFile(InputStream data, FileOutputStream fos)
+ throws IOException {
+ byte[] buffer = new byte[32768];
+ int amt;
+ while ((amt=data.read(buffer)) > 0) {
+ fos.write(buffer, 0, amt);
+ }
+ }
+
+ /**
+ * Change the current system wallpaper to a specific byte stream, specifying a
+ * hint about which subrectangle of the full image is to be visible. The OS will
+ * then try to best present the given portion of the full image as the static system
+ * wallpaper image. The data from the given InputStream is copied into persistent
+ * storage and will then be used as the system wallpaper. Currently the data must
+ * be either a JPEG or PNG image. On success, the intent
+ * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @param bitmapData A stream containing the raw data to install as a wallpaper. This
+ * data can be in any format handled by {@link BitmapRegionDecoder}.
+ * @param visibleCropHint The rectangular subregion of the streamed image that should be
+ * displayed as wallpaper. Passing {@code null} for this parameter means that
+ * the full image should be displayed if possible given the image's and device's
+ * aspect ratios, etc.
+ * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper
+ * image for restore to a future device; {@code false} otherwise.
+ * @return An integer ID assigned to the newly active wallpaper; or zero on failure.
+ *
+ * @see #getWallpaperId(int)
+ *
+ * @throws IOException If an error occurs when attempting to set the wallpaper
+ * based on the provided image data.
+ * @throws IllegalArgumentException If the {@code visibleCropHint} rectangle is
+ * empty or invalid.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setStream(InputStream bitmapData, Rect visibleCropHint, boolean allowBackup)
+ throws IOException {
+ return setStream(bitmapData, visibleCropHint, allowBackup, FLAG_SYSTEM | FLAG_LOCK);
+ }
+
+ /**
+ * Version of {@link #setStream(InputStream, Rect, boolean)} that allows the caller
+ * to specify which of the supported wallpaper categories to set.
+ *
+ * @param bitmapData A stream containing the raw data to install as a wallpaper. This
+ * data can be in any format handled by {@link BitmapRegionDecoder}.
+ * @param visibleCropHint The rectangular subregion of the streamed image that should be
+ * displayed as wallpaper. Passing {@code null} for this parameter means that
+ * the full image should be displayed if possible given the image's and device's
+ * aspect ratios, etc.
+ * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper
+ * image for restore to a future device; {@code false} otherwise.
+ * @param which Flags indicating which wallpaper(s) to configure with the new imagery.
+ * @return An integer ID assigned to the newly active wallpaper; or zero on failure.
+ *
+ * @see #getWallpaperId(int)
+ * @see #FLAG_LOCK
+ * @see #FLAG_SYSTEM
+ *
+ * @throws IOException
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public int setStream(InputStream bitmapData, Rect visibleCropHint,
+ boolean allowBackup, @SetWallpaperFlags int which)
+ throws IOException {
+ validateRect(visibleCropHint);
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ final Bundle result = new Bundle();
+ final WallpaperSetCompletion completion = new WallpaperSetCompletion();
+ try {
+ ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
+ mContext.getOpPackageName(), visibleCropHint, allowBackup,
+ result, which, completion, UserHandle.myUserId());
+ if (fd != null) {
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ copyStreamToWallpaperFile(bitmapData, fos);
+ fos.close();
+ completion.waitForCompletion();
+ } finally {
+ IoUtils.closeQuietly(fos);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);
+ }
+
+ /**
+ * Return whether any users are currently set to use the wallpaper
+ * with the given resource ID. That is, their wallpaper has been
+ * set through {@link #setResource(int)} with the same resource id.
+ */
+ public boolean hasResourceWallpaper(@RawRes int resid) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ Resources resources = mContext.getResources();
+ String name = "res:" + resources.getResourceName(resid);
+ return sGlobals.mService.hasNamedWallpaper(name);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the desired minimum width for the wallpaper. Callers of
+ * {@link #setBitmap(android.graphics.Bitmap)} or
+ * {@link #setStream(java.io.InputStream)} should check this value
+ * beforehand to make sure the supplied wallpaper respects the desired
+ * minimum width.
+ *
+ * If the returned value is <= 0, the caller should use the width of
+ * the default display instead.
+ *
+ * @return The desired minimum width for the wallpaper. This value should
+ * be honored by applications that set the wallpaper but it is not
+ * mandatory.
+ */
+ public int getDesiredMinimumWidth() {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ return sGlobals.mService.getWidthHint();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the desired minimum height for the wallpaper. Callers of
+ * {@link #setBitmap(android.graphics.Bitmap)} or
+ * {@link #setStream(java.io.InputStream)} should check this value
+ * beforehand to make sure the supplied wallpaper respects the desired
+ * minimum height.
+ *
+ * If the returned value is <= 0, the caller should use the height of
+ * the default display instead.
+ *
+ * @return The desired minimum height for the wallpaper. This value should
+ * be honored by applications that set the wallpaper but it is not
+ * mandatory.
+ */
+ public int getDesiredMinimumHeight() {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ return sGlobals.mService.getHeightHint();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * For use only by the current home application, to specify the size of
+ * wallpaper it would like to use. This allows such applications to have
+ * a virtual wallpaper that is larger than the physical screen, matching
+ * the size of their workspace.
+ *
+ * <p>Note developers, who don't seem to be reading this. This is
+ * for <em>home apps</em> to tell what size wallpaper they would like.
+ * Nobody else should be calling this! Certainly not other non-home
+ * apps that change the wallpaper. Those apps are supposed to
+ * <b>retrieve</b> the suggested size so they can construct a wallpaper
+ * that matches it.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER_HINTS}.
+ *
+ * @param minimumWidth Desired minimum width
+ * @param minimumHeight Desired minimum height
+ */
+ public void suggestDesiredDimensions(int minimumWidth, int minimumHeight) {
+ try {
+ /**
+ * The framework makes no attempt to limit the window size
+ * to the maximum texture size. Any window larger than this
+ * cannot be composited.
+ *
+ * Read maximum texture size from system property and scale down
+ * minimumWidth and minimumHeight accordingly.
+ */
+ int maximumTextureSize;
+ try {
+ maximumTextureSize = SystemProperties.getInt("sys.max_texture_size", 0);
+ } catch (Exception e) {
+ maximumTextureSize = 0;
+ }
+
+ if (maximumTextureSize > 0) {
+ if ((minimumWidth > maximumTextureSize) ||
+ (minimumHeight > maximumTextureSize)) {
+ float aspect = (float)minimumHeight / (float)minimumWidth;
+ if (minimumWidth > minimumHeight) {
+ minimumWidth = maximumTextureSize;
+ minimumHeight = (int)((minimumWidth * aspect) + 0.5);
+ } else {
+ minimumHeight = maximumTextureSize;
+ minimumWidth = (int)((minimumHeight / aspect) + 0.5);
+ }
+ }
+ }
+
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ sGlobals.mService.setDimensionHints(minimumWidth, minimumHeight,
+ mContext.getOpPackageName());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Specify extra padding that the wallpaper should have outside of the display.
+ * That is, the given padding supplies additional pixels the wallpaper should extend
+ * outside of the display itself.
+ * @param padding The number of pixels the wallpaper should extend beyond the display,
+ * on its left, top, right, and bottom sides.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_HINTS)
+ public void setDisplayPadding(Rect padding) {
+ try {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ sGlobals.mService.setDisplayPadding(padding, mContext.getOpPackageName());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Apply a raw offset to the wallpaper window. Should only be used in
+ * combination with {@link #setDisplayPadding(android.graphics.Rect)} when you
+ * have ensured that the wallpaper will extend outside of the display area so that
+ * it can be moved without leaving part of the display uncovered.
+ * @param x The offset, in pixels, to apply to the left edge.
+ * @param y The offset, in pixels, to apply to the top edge.
+ * @hide
+ */
+ @SystemApi
+ public void setDisplayOffset(IBinder windowToken, int x, int y) {
+ try {
+ //Log.v(TAG, "Sending new wallpaper display offsets from app...");
+ WindowManagerGlobal.getWindowSession().setWallpaperDisplayOffset(
+ windowToken, x, y);
+ //Log.v(TAG, "...app returning after sending display offset!");
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Clear the wallpaper.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void clearWallpaper() {
+ clearWallpaper(FLAG_LOCK, mContext.getUserId());
+ clearWallpaper(FLAG_SYSTEM, mContext.getUserId());
+ }
+
+ /**
+ * Clear the wallpaper for a specific user. The caller must hold the
+ * INTERACT_ACROSS_USERS_FULL permission to clear another user's
+ * wallpaper, and must hold the SET_WALLPAPER permission in all
+ * circumstances.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ public void clearWallpaper(@SetWallpaperFlags int which, int userId) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ sGlobals.mService.clearWallpaper(mContext.getOpPackageName(), which, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the live wallpaper.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT)
+ public boolean setWallpaperComponent(ComponentName name) {
+ return setWallpaperComponent(name, UserHandle.myUserId());
+ }
+
+ /**
+ * Set the live wallpaper.
+ *
+ * This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT
+ * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change
+ * another user's wallpaper.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT)
+ public boolean setWallpaperComponent(ComponentName name, int userId) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ sGlobals.mService.setWallpaperComponentChecked(name, mContext.getOpPackageName(),
+ userId);
+ return true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the display position of the current wallpaper within any larger space, when
+ * that wallpaper is visible behind the given window. The X and Y offsets
+ * are floating point numbers ranging from 0 to 1, representing where the
+ * wallpaper should be positioned within the screen space. These only
+ * make sense when the wallpaper is larger than the display.
+ *
+ * @param windowToken The window who these offsets should be associated
+ * with, as returned by {@link android.view.View#getWindowToken()
+ * View.getWindowToken()}.
+ * @param xOffset The offset along the X dimension, from 0 to 1.
+ * @param yOffset The offset along the Y dimension, from 0 to 1.
+ */
+ public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) {
+ try {
+ //Log.v(TAG, "Sending new wallpaper offsets from app...");
+ WindowManagerGlobal.getWindowSession().setWallpaperPosition(
+ windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep);
+ //Log.v(TAG, "...app returning after sending offsets!");
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * For applications that use multiple virtual screens showing a wallpaper,
+ * specify the step size between virtual screens. For example, if the
+ * launcher has 3 virtual screens, it would specify an xStep of 0.5,
+ * since the X offset for those screens are 0.0, 0.5 and 1.0
+ * @param xStep The X offset delta from one screen to the next one
+ * @param yStep The Y offset delta from one screen to the next one
+ */
+ public void setWallpaperOffsetSteps(float xStep, float yStep) {
+ mWallpaperXStep = xStep;
+ mWallpaperYStep = yStep;
+ }
+
+ /**
+ * Send an arbitrary command to the current active wallpaper.
+ *
+ * @param windowToken The window who these offsets should be associated
+ * with, as returned by {@link android.view.View#getWindowToken()
+ * View.getWindowToken()}.
+ * @param action Name of the command to perform. This must be a scoped
+ * name to avoid collisions, such as "com.mycompany.wallpaper.DOIT".
+ * @param x Arbitrary integer argument based on command.
+ * @param y Arbitrary integer argument based on command.
+ * @param z Arbitrary integer argument based on command.
+ * @param extras Optional additional information for the command, or null.
+ */
+ public void sendWallpaperCommand(IBinder windowToken, String action,
+ int x, int y, int z, Bundle extras) {
+ try {
+ //Log.v(TAG, "Sending new wallpaper offsets from app...");
+ WindowManagerGlobal.getWindowSession().sendWallpaperCommand(
+ windowToken, action, x, y, z, extras, false);
+ //Log.v(TAG, "...app returning after sending offsets!");
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether wallpapers are supported for the calling user. If this function returns
+ * {@code false}, any attempts to changing the wallpaper will have no effect,
+ * and any attempt to obtain of the wallpaper will return {@code null}.
+ */
+ public boolean isWallpaperSupported() {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ try {
+ return sGlobals.mService.isWallpaperSupported(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns whether the calling package is allowed to set the wallpaper for the calling user.
+ * If this function returns {@code false}, any attempts to change the wallpaper will have
+ * no effect. Always returns {@code true} for device owner and profile owner.
+ *
+ * @see android.os.UserManager#DISALLOW_SET_WALLPAPER
+ */
+ public boolean isSetWallpaperAllowed() {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ } else {
+ try {
+ return sGlobals.mService.isSetWallpaperAllowed(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Clear the offsets previously associated with this window through
+ * {@link #setWallpaperOffsets(IBinder, float, float)}. This reverts
+ * the window to its default state, where it does not cause the wallpaper
+ * to scroll from whatever its last offsets were.
+ *
+ * @param windowToken The window who these offsets should be associated
+ * with, as returned by {@link android.view.View#getWindowToken()
+ * View.getWindowToken()}.
+ */
+ public void clearWallpaperOffsets(IBinder windowToken) {
+ try {
+ WindowManagerGlobal.getWindowSession().setWallpaperPosition(
+ windowToken, -1, -1, -1, -1);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove any currently set system wallpaper, reverting to the system's built-in
+ * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
+ * is broadcast.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#SET_WALLPAPER}.
+ *
+ * @throws IOException If an error occurs reverting to the built-in
+ * wallpaper.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void clear() throws IOException {
+ setStream(openDefaultWallpaper(mContext, FLAG_SYSTEM), null, false);
+ }
+
+ /**
+ * Remove one or more currently set wallpapers, reverting to the system default
+ * display for each one. If {@link #FLAG_SYSTEM} is set in the {@code which}
+ * parameter, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} will be broadcast
+ * upon success.
+ *
+ * @param which A bitwise combination of {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}
+ * @throws IOException If an error occurs reverting to the built-in wallpaper.
+ */
+ @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
+ public void clear(@SetWallpaperFlags int which) throws IOException {
+ if ((which & FLAG_SYSTEM) != 0) {
+ clear();
+ }
+ if ((which & FLAG_LOCK) != 0) {
+ clearWallpaper(FLAG_LOCK, mContext.getUserId());
+ }
+ }
+
+ /**
+ * Open stream representing the default static image wallpaper.
+ *
+ * If the device defines no default wallpaper of the requested kind,
+ * {@code null} is returned.
+ *
+ * @hide
+ */
+ public static InputStream openDefaultWallpaper(Context context, @SetWallpaperFlags int which) {
+ final String whichProp;
+ final int defaultResId;
+ if (which == FLAG_LOCK) {
+ /* Factory-default lock wallpapers are not yet supported
+ whichProp = PROP_LOCK_WALLPAPER;
+ defaultResId = com.android.internal.R.drawable.default_lock_wallpaper;
+ */
+ return null;
+ } else {
+ whichProp = PROP_WALLPAPER;
+ defaultResId = com.android.internal.R.drawable.default_wallpaper;
+ }
+ final String path = SystemProperties.get(whichProp);
+ if (!TextUtils.isEmpty(path)) {
+ final File file = new File(path);
+ if (file.exists()) {
+ try {
+ return new FileInputStream(file);
+ } catch (IOException e) {
+ // Ignored, fall back to platform default below
+ }
+ }
+ }
+ try {
+ return context.getResources().openRawResource(defaultResId);
+ } catch (NotFoundException e) {
+ // no default defined for this device; this is not a failure
+ }
+ return null;
+ }
+
+ /**
+ * Return {@link ComponentName} of the default live wallpaper, or
+ * {@code null} if none is defined.
+ *
+ * @hide
+ */
+ public static ComponentName getDefaultWallpaperComponent(Context context) {
+ String flat = SystemProperties.get(PROP_WALLPAPER_COMPONENT);
+ if (!TextUtils.isEmpty(flat)) {
+ final ComponentName cn = ComponentName.unflattenFromString(flat);
+ if (cn != null) {
+ return cn;
+ }
+ }
+
+ flat = context.getString(com.android.internal.R.string.default_wallpaper_component);
+ if (!TextUtils.isEmpty(flat)) {
+ final ComponentName cn = ComponentName.unflattenFromString(flat);
+ if (cn != null) {
+ return cn;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Register a callback for lock wallpaper observation. Only the OS may use this.
+ *
+ * @return true on success; false on error.
+ * @hide
+ */
+ public boolean setLockWallpaperCallback(IWallpaperManagerCallback callback) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+
+ try {
+ return sGlobals.mService.setLockWallpaperCallback(callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Is the current system wallpaper eligible for backup?
+ *
+ * Only the OS itself may use this method.
+ * @hide
+ */
+ public boolean isWallpaperBackupEligible(int which) {
+ if (sGlobals.mService == null) {
+ Log.w(TAG, "WallpaperService not running");
+ throw new RuntimeException(new DeadSystemException());
+ }
+ try {
+ return sGlobals.mService.isWallpaperBackupEligible(which, mContext.getUserId());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception querying wallpaper backup eligibility: " + e.getMessage());
+ }
+ return false;
+ }
+
+ // Private completion callback for setWallpaper() synchronization
+ private class WallpaperSetCompletion extends IWallpaperManagerCallback.Stub {
+ final CountDownLatch mLatch;
+
+ public WallpaperSetCompletion() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ public void waitForCompletion() {
+ try {
+ mLatch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ // This might be legit: the crop may take a very long time. Don't sweat
+ // it in that case; we are okay with display lagging behind in order to
+ // keep the caller from locking up indeterminately.
+ }
+ }
+
+ @Override
+ public void onWallpaperChanged() throws RemoteException {
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onWallpaperColorsChanged(WallpaperColors colors, int which, int userId)
+ throws RemoteException {
+ sGlobals.onWallpaperColorsChanged(colors, which, userId);
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when colors change on a wallpaper.
+ */
+ public interface OnColorsChangedListener {
+ /**
+ * Called when colors change.
+ * A {@link android.app.WallpaperColors} object containing a simplified
+ * color histogram will be given.
+ *
+ * @param colors Wallpaper color info
+ * @param which A combination of {@link #FLAG_LOCK} and {@link #FLAG_SYSTEM}
+ */
+ void onColorsChanged(WallpaperColors colors, int which);
+
+ /**
+ * Called when colors change.
+ * A {@link android.app.WallpaperColors} object containing a simplified
+ * color histogram will be given.
+ *
+ * @param colors Wallpaper color info
+ * @param which A combination of {@link #FLAG_LOCK} and {@link #FLAG_SYSTEM}
+ * @param userId Owner of the wallpaper
+ * @hide
+ */
+ default void onColorsChanged(WallpaperColors colors, int which, int userId) {
+ onColorsChanged(colors, which);
+ }
+ }
+}
diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java
new file mode 100644
index 00000000..5d87e1c2
--- /dev/null
+++ b/android/app/WindowConfiguration.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.ActivityThread.isSystem;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.DisplayInfo;
+
+/**
+ * Class that contains windowing configuration/state for other objects that contain windows directly
+ * or indirectly. E.g. Activities, Task, Displays, ...
+ * The test class is {@link com.android.server.wm.WindowConfigurationTests} which must be kept
+ * up-to-date and ran anytime changes are made to this class.
+ * @hide
+ */
+@TestApi
+public class WindowConfiguration implements Parcelable, Comparable<WindowConfiguration> {
+
+ /**
+ * {@link android.graphics.Rect} defining app bounds. The dimensions override usages of
+ * {@link DisplayInfo#appHeight} and {@link DisplayInfo#appWidth} and mirrors these values at
+ * the display level. Lower levels can override these values to provide custom bounds to enforce
+ * features such as a max aspect ratio.
+ */
+ private Rect mAppBounds;
+
+ /** The current windowing mode of the configuration. */
+ private @WindowingMode int mWindowingMode;
+
+ /** Windowing mode is currently not defined.
+ * @hide */
+ public static final int WINDOWING_MODE_UNDEFINED = 0;
+ /** Occupies the full area of the screen or the parent container.
+ * @hide */
+ public static final int WINDOWING_MODE_FULLSCREEN = 1;
+ /** Always on-top (always visible). of other siblings in its parent container.
+ * @hide */
+ public static final int WINDOWING_MODE_PINNED = 2;
+ /** The primary container driving the screen to be in split-screen mode.
+ * @hide */
+ public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3;
+ /**
+ * The containers adjacent to the {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} container in
+ * split-screen mode.
+ * @hide
+ */
+ public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4;
+ /** Can be freely resized within its parent container.
+ * @hide */
+ public static final int WINDOWING_MODE_FREEFORM = 5;
+
+ /** @hide */
+ @IntDef({
+ WINDOWING_MODE_UNDEFINED,
+ WINDOWING_MODE_FULLSCREEN,
+ WINDOWING_MODE_PINNED,
+ WINDOWING_MODE_SPLIT_SCREEN_PRIMARY,
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
+ WINDOWING_MODE_FREEFORM,
+ })
+ public @interface WindowingMode {}
+
+ /** The current activity type of the configuration. */
+ private @ActivityType int mActivityType;
+
+ /** Activity type is currently not defined.
+ * @hide */
+ public static final int ACTIVITY_TYPE_UNDEFINED = 0;
+ /** Standard activity type. Nothing special about the activity...
+ * @hide */
+ public static final int ACTIVITY_TYPE_STANDARD = 1;
+ /** Home/Launcher activity type. */
+ public static final int ACTIVITY_TYPE_HOME = 2;
+ /** Recents/Overview activity type. */
+ public static final int ACTIVITY_TYPE_RECENTS = 3;
+ /** Assistant activity type.
+ * @hide */
+ public static final int ACTIVITY_TYPE_ASSISTANT = 4;
+
+ /** @hide */
+ @IntDef({
+ ACTIVITY_TYPE_UNDEFINED,
+ ACTIVITY_TYPE_STANDARD,
+ ACTIVITY_TYPE_HOME,
+ ACTIVITY_TYPE_RECENTS,
+ ACTIVITY_TYPE_ASSISTANT,
+ })
+ public @interface ActivityType {}
+
+ /** Bit that indicates that the {@link #mAppBounds} changed.
+ * @hide */
+ public static final int WINDOW_CONFIG_APP_BOUNDS = 1 << 0;
+ /** Bit that indicates that the {@link #mWindowingMode} changed.
+ * @hide */
+ public static final int WINDOW_CONFIG_WINDOWING_MODE = 1 << 1;
+ /** Bit that indicates that the {@link #mActivityType} changed.
+ * @hide */
+ public static final int WINDOW_CONFIG_ACTIVITY_TYPE = 1 << 2;
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ WINDOW_CONFIG_APP_BOUNDS,
+ WINDOW_CONFIG_WINDOWING_MODE,
+ WINDOW_CONFIG_ACTIVITY_TYPE,
+ })
+ public @interface WindowConfig {}
+
+ /** @hide */
+ public WindowConfiguration() {
+ unset();
+ }
+
+ /** @hide */
+ public WindowConfiguration(WindowConfiguration configuration) {
+ setTo(configuration);
+ }
+
+ private WindowConfiguration(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mAppBounds, flags);
+ dest.writeInt(mWindowingMode);
+ dest.writeInt(mActivityType);
+ }
+
+ private void readFromParcel(Parcel source) {
+ mAppBounds = source.readParcelable(Rect.class.getClassLoader());
+ mWindowingMode = source.readInt();
+ mActivityType = source.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ public static final Creator<WindowConfiguration> CREATOR = new Creator<WindowConfiguration>() {
+ @Override
+ public WindowConfiguration createFromParcel(Parcel in) {
+ return new WindowConfiguration(in);
+ }
+
+ @Override
+ public WindowConfiguration[] newArray(int size) {
+ return new WindowConfiguration[size];
+ }
+ };
+
+ /**
+ * Set {@link #mAppBounds} to the input Rect.
+ * @param rect The rect value to set {@link #mAppBounds} to.
+ * @see #getAppBounds()
+ * @hide
+ */
+ public void setAppBounds(Rect rect) {
+ if (rect == null) {
+ mAppBounds = null;
+ return;
+ }
+
+ setAppBounds(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ /**
+ * @see #setAppBounds(Rect)
+ * @see #getAppBounds()
+ * @hide
+ */
+ public void setAppBounds(int left, int top, int right, int bottom) {
+ if (mAppBounds == null) {
+ mAppBounds = new Rect();
+ }
+
+ mAppBounds.set(left, top, right, bottom);
+ }
+
+ /**
+ * @see #setAppBounds(Rect)
+ * @hide
+ */
+ public Rect getAppBounds() {
+ return mAppBounds;
+ }
+
+ /** @hide */
+ public void setWindowingMode(@WindowingMode int windowingMode) {
+ mWindowingMode = windowingMode;
+ }
+
+ /** @hide */
+ @WindowingMode
+ public int getWindowingMode() {
+ return mWindowingMode;
+ }
+
+ /** @hide */
+ public void setActivityType(@ActivityType int activityType) {
+ if (mActivityType == activityType) {
+ return;
+ }
+
+ // Error check within system server that we are not changing activity type which can be
+ // dangerous. It is okay for things to change in the application process as it doesn't
+ // affect how other things is the system is managed.
+ if (isSystem()
+ && mActivityType != ACTIVITY_TYPE_UNDEFINED
+ && activityType != ACTIVITY_TYPE_UNDEFINED) {
+ throw new IllegalStateException("Can't change activity type once set: " + this
+ + " activityType=" + activityTypeToString(activityType));
+ }
+ mActivityType = activityType;
+ }
+
+ /** @hide */
+ @ActivityType
+ public int getActivityType() {
+ return mActivityType;
+ }
+
+ /** @hide */
+ public void setTo(WindowConfiguration other) {
+ setAppBounds(other.mAppBounds);
+ setWindowingMode(other.mWindowingMode);
+ setActivityType(other.mActivityType);
+ }
+
+ /** Set this object to completely undefined.
+ * @hide */
+ public void unset() {
+ setToDefaults();
+ }
+
+ /** @hide */
+ public void setToDefaults() {
+ setAppBounds(null);
+ setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Copies the fields from delta into this Configuration object, keeping
+ * track of which ones have changed. Any undefined fields in {@code delta}
+ * are ignored and not copied in to the current Configuration.
+ *
+ * @return a bit mask of the changed fields, as per {@link #diff}
+ * @hide
+ */
+ public @WindowConfig int updateFrom(@NonNull WindowConfiguration delta) {
+ int changed = 0;
+ if (delta.mAppBounds != null && !delta.mAppBounds.equals(mAppBounds)) {
+ changed |= WINDOW_CONFIG_APP_BOUNDS;
+ setAppBounds(delta.mAppBounds);
+ }
+ if (delta.mWindowingMode != WINDOWING_MODE_UNDEFINED
+ && mWindowingMode != delta.mWindowingMode) {
+ changed |= WINDOW_CONFIG_WINDOWING_MODE;
+ setWindowingMode(delta.mWindowingMode);
+ }
+ if (delta.mActivityType != ACTIVITY_TYPE_UNDEFINED
+ && mActivityType != delta.mActivityType) {
+ changed |= WINDOW_CONFIG_ACTIVITY_TYPE;
+ setActivityType(delta.mActivityType);
+ }
+ return changed;
+ }
+
+ /**
+ * Return a bit mask of the differences between this Configuration object and the given one.
+ * Does not change the values of either. Any undefined fields in <var>other</var> are ignored.
+ * @param other The configuration to diff against.
+ * @param compareUndefined If undefined values should be compared.
+ * @return Returns a bit mask indicating which configuration
+ * values has changed, containing any combination of {@link WindowConfig} flags.
+ *
+ * @see Configuration#diff(Configuration)
+ * @hide
+ */
+ public @WindowConfig long diff(WindowConfiguration other, boolean compareUndefined) {
+ long changes = 0;
+
+ // Make sure that one of the values is not null and that they are not equal.
+ if ((compareUndefined || other.mAppBounds != null)
+ && mAppBounds != other.mAppBounds
+ && (mAppBounds == null || !mAppBounds.equals(other.mAppBounds))) {
+ changes |= WINDOW_CONFIG_APP_BOUNDS;
+ }
+
+ if ((compareUndefined || other.mWindowingMode != WINDOWING_MODE_UNDEFINED)
+ && mWindowingMode != other.mWindowingMode) {
+ changes |= WINDOW_CONFIG_WINDOWING_MODE;
+ }
+
+ if ((compareUndefined || other.mActivityType != ACTIVITY_TYPE_UNDEFINED)
+ && mActivityType != other.mActivityType) {
+ changes |= WINDOW_CONFIG_ACTIVITY_TYPE;
+ }
+
+ return changes;
+ }
+
+ @Override
+ public int compareTo(WindowConfiguration that) {
+ int n = 0;
+ if (mAppBounds == null && that.mAppBounds != null) {
+ return 1;
+ } else if (mAppBounds != null && that.mAppBounds == null) {
+ return -1;
+ } else if (mAppBounds != null && that.mAppBounds != null) {
+ n = mAppBounds.left - that.mAppBounds.left;
+ if (n != 0) return n;
+ n = mAppBounds.top - that.mAppBounds.top;
+ if (n != 0) return n;
+ n = mAppBounds.right - that.mAppBounds.right;
+ if (n != 0) return n;
+ n = mAppBounds.bottom - that.mAppBounds.bottom;
+ if (n != 0) return n;
+ }
+ n = mWindowingMode - that.mWindowingMode;
+ if (n != 0) return n;
+ n = mActivityType - that.mActivityType;
+ if (n != 0) return n;
+
+ // if (n != 0) return n;
+ return n;
+ }
+
+ /** @hide */
+ @Override
+ public boolean equals(Object that) {
+ if (that == null) return false;
+ if (that == this) return true;
+ if (!(that instanceof WindowConfiguration)) {
+ return false;
+ }
+ return this.compareTo((WindowConfiguration) that) == 0;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ int result = 0;
+ if (mAppBounds != null) {
+ result = 31 * result + mAppBounds.hashCode();
+ }
+ result = 31 * result + mWindowingMode;
+ result = 31 * result + mActivityType;
+ return result;
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return "{mAppBounds=" + mAppBounds
+ + " mWindowingMode=" + windowingModeToString(mWindowingMode)
+ + " mActivityType=" + activityTypeToString(mActivityType) + "}";
+ }
+
+ /**
+ * Returns true if the activities associated with this window configuration display a shadow
+ * around their border.
+ * @hide
+ */
+ public boolean hasWindowShadow() {
+ return tasksAreFloating();
+ }
+
+ /**
+ * Returns true if the activities associated with this window configuration display a decor
+ * view.
+ * @hide
+ */
+ public boolean hasWindowDecorCaption() {
+ return mWindowingMode == WINDOWING_MODE_FREEFORM;
+ }
+
+ /**
+ * Returns true if the tasks associated with this window configuration can be resized
+ * independently of their parent container.
+ * @hide
+ */
+ public boolean canResizeTask() {
+ return mWindowingMode == WINDOWING_MODE_FREEFORM;
+ }
+
+ /** Returns true if the task bounds should persist across power cycles.
+ * @hide */
+ public boolean persistTaskBounds() {
+ return mWindowingMode == WINDOWING_MODE_FREEFORM;
+ }
+
+ /**
+ * Returns true if the tasks associated with this window configuration are floating.
+ * Floating tasks are laid out differently as they are allowed to extend past the display bounds
+ * without overscan insets.
+ * @hide
+ */
+ public boolean tasksAreFloating() {
+ return mWindowingMode == WINDOWING_MODE_FREEFORM || mWindowingMode == WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if the windows associated with this window configuration can receive input keys.
+ * @hide
+ */
+ public boolean canReceiveKeys() {
+ return mWindowingMode != WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if the container associated with this window configuration is always-on-top of
+ * its siblings.
+ * @hide
+ */
+ public boolean isAlwaysOnTop() {
+ return mWindowingMode == WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if any visible windows belonging to apps with this window configuration should
+ * be kept on screen when the app is killed due to something like the low memory killer.
+ * @hide
+ */
+ public boolean keepVisibleDeadAppWindowOnScreen() {
+ return mWindowingMode != WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if the backdrop on the client side should match the frame of the window.
+ * Returns false, if the backdrop should be fullscreen.
+ * @hide
+ */
+ public boolean useWindowFrameForBackdrop() {
+ return mWindowingMode == WINDOWING_MODE_FREEFORM || mWindowingMode == WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if this container may be scaled without resizing, and windows within may need
+ * to be configured as such.
+ * @hide
+ */
+ public boolean windowsAreScaleable() {
+ return mWindowingMode == WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if windows in this container should be given move animations by default.
+ * @hide
+ */
+ public boolean hasMovementAnimations() {
+ return mWindowingMode != WINDOWING_MODE_PINNED;
+ }
+
+ /**
+ * Returns true if this container can be put in either
+ * {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} or
+ * {@link #WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} windowing modes based on its current state.
+ * @hide
+ */
+ public boolean supportSplitScreenWindowingMode() {
+ if (mActivityType == ACTIVITY_TYPE_ASSISTANT) {
+ return false;
+ }
+ return mWindowingMode != WINDOWING_MODE_FREEFORM && mWindowingMode != WINDOWING_MODE_PINNED;
+ }
+
+ private static String windowingModeToString(@WindowingMode int windowingMode) {
+ switch (windowingMode) {
+ case WINDOWING_MODE_UNDEFINED: return "undefined";
+ case WINDOWING_MODE_FULLSCREEN: return "fullscreen";
+ case WINDOWING_MODE_PINNED: return "pinned";
+ case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: return "split-screen-primary";
+ case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: return "split-screen-secondary";
+ case WINDOWING_MODE_FREEFORM: return "freeform";
+ }
+ return String.valueOf(windowingMode);
+ }
+
+ /** @hide */
+ public static String activityTypeToString(@ActivityType int applicationType) {
+ switch (applicationType) {
+ case ACTIVITY_TYPE_UNDEFINED: return "undefined";
+ case ACTIVITY_TYPE_STANDARD: return "standard";
+ case ACTIVITY_TYPE_HOME: return "home";
+ case ACTIVITY_TYPE_RECENTS: return "recents";
+ case ACTIVITY_TYPE_ASSISTANT: return "assistant";
+ }
+ return String.valueOf(applicationType);
+ }
+}
diff --git a/android/app/admin/ConnectEvent.java b/android/app/admin/ConnectEvent.java
new file mode 100644
index 00000000..ffd38e2b
--- /dev/null
+++ b/android/app/admin/ConnectEvent.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * A class that represents a TCP connect event initiated through the standard network stack.
+ *
+ * <p>It contains information about the originating app as well as the remote TCP endpoint.
+ *
+ * <p>Support both IPv4 and IPv6 connections.
+ */
+public final class ConnectEvent extends NetworkEvent implements Parcelable {
+
+ /** The destination IP address. */
+ private final String ipAddress;
+
+ /** The destination port number. */
+ private final int port;
+
+ /** @hide */
+ public ConnectEvent(String ipAddress, int port, String packageName, long timestamp) {
+ super(packageName, timestamp);
+ this.ipAddress = ipAddress;
+ this.port = port;
+ }
+
+ private ConnectEvent(Parcel in) {
+ this.ipAddress = in.readString();
+ this.port = in.readInt();
+ this.packageName = in.readString();
+ this.timestamp = in.readLong();
+ }
+
+ public InetAddress getInetAddress() {
+ try {
+ // ipAddress is already an address, not a host name, no DNS resolution will happen.
+ return InetAddress.getByName(ipAddress);
+ } catch (UnknownHostException e) {
+ // Should never happen as we aren't passing a host name.
+ return InetAddress.getLoopbackAddress();
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ConnectEvent(%s, %d, %d, %s)", ipAddress, port, timestamp,
+ packageName);
+ }
+
+ public static final Parcelable.Creator<ConnectEvent> CREATOR
+ = new Parcelable.Creator<ConnectEvent>() {
+ @Override
+ public ConnectEvent createFromParcel(Parcel in) {
+ if (in.readInt() != PARCEL_TOKEN_CONNECT_EVENT) {
+ return null;
+ }
+ return new ConnectEvent(in);
+ }
+
+ @Override
+ public ConnectEvent[] newArray(int size) {
+ return new ConnectEvent[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // write parcel token first
+ out.writeInt(PARCEL_TOKEN_CONNECT_EVENT);
+ out.writeString(ipAddress);
+ out.writeInt(port);
+ out.writeString(packageName);
+ out.writeLong(timestamp);
+ }
+}
+
diff --git a/android/app/admin/DeviceAdminInfo.java b/android/app/admin/DeviceAdminInfo.java
new file mode 100644
index 00000000..1de1d2fb
--- /dev/null
+++ b/android/app/admin/DeviceAdminInfo.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This class is used to specify meta information of a device administrator
+ * component.
+ */
+public final class DeviceAdminInfo implements Parcelable {
+ static final String TAG = "DeviceAdminInfo";
+
+ /**
+ * A type of policy that this device admin can use: device owner meta-policy
+ * for an admin that is designated as owner of the device.
+ *
+ * @hide
+ */
+ public static final int USES_POLICY_DEVICE_OWNER = -2;
+
+ /**
+ * A type of policy that this device admin can use: profile owner meta-policy
+ * for admins that have been installed as owner of some user profile.
+ *
+ * @hide
+ */
+ public static final int USES_POLICY_PROFILE_OWNER = -1;
+
+ /**
+ * A type of policy that this device admin can use: limit the passwords
+ * that the user can select, via {@link DevicePolicyManager#setPasswordQuality}
+ * and {@link DevicePolicyManager#setPasswordMinimumLength}.
+ *
+ * <p>To control this policy, the device admin must have a "limit-password"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_LIMIT_PASSWORD = 0;
+
+ /**
+ * A type of policy that this device admin can use: able to watch login
+ * attempts from the user, via {@link DeviceAdminReceiver#ACTION_PASSWORD_FAILED},
+ * {@link DeviceAdminReceiver#ACTION_PASSWORD_SUCCEEDED}, and
+ * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts}.
+ *
+ * <p>To control this policy, the device admin must have a "watch-login"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_WATCH_LOGIN = 1;
+
+ /**
+ * A type of policy that this device admin can use: able to reset the
+ * user's password via
+ * {@link DevicePolicyManager#resetPassword}.
+ *
+ * <p>To control this policy, the device admin must have a "reset-password"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_RESET_PASSWORD = 2;
+
+ /**
+ * A type of policy that this device admin can use: able to force the device
+ * to lock via{@link DevicePolicyManager#lockNow} or limit the
+ * maximum lock timeout for the device via
+ * {@link DevicePolicyManager#setMaximumTimeToLock}.
+ *
+ * <p>To control this policy, the device admin must have a "force-lock"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_FORCE_LOCK = 3;
+
+ /**
+ * A type of policy that this device admin can use: able to factory
+ * reset the device, erasing all of the user's data, via
+ * {@link DevicePolicyManager#wipeData}.
+ *
+ * <p>To control this policy, the device admin must have a "wipe-data"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_WIPE_DATA = 4;
+
+ /**
+ * A type of policy that this device admin can use: able to specify the
+ * device Global Proxy, via {@link DevicePolicyManager#setGlobalProxy}.
+ *
+ * <p>To control this policy, the device admin must have a "set-global-proxy"
+ * tag in the "uses-policies" section of its meta-data.
+ * @hide
+ */
+ public static final int USES_POLICY_SETS_GLOBAL_PROXY = 5;
+
+ /**
+ * A type of policy that this device admin can use: force the user to
+ * change their password after an administrator-defined time limit.
+ *
+ * <p>To control this policy, the device admin must have an "expire-password"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_EXPIRE_PASSWORD = 6;
+
+ /**
+ * A type of policy that this device admin can use: require encryption of stored data.
+ *
+ * <p>To control this policy, the device admin must have a "encrypted-storage"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_ENCRYPTED_STORAGE = 7;
+
+ /**
+ * A type of policy that this device admin can use: disables use of all device cameras.
+ *
+ * <p>To control this policy, the device admin must have a "disable-camera"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_DISABLE_CAMERA = 8;
+
+ /**
+ * A type of policy that this device admin can use: disables use of keyguard features.
+ *
+ * <p>To control this policy, the device admin must have a "disable-keyguard-features"
+ * tag in the "uses-policies" section of its meta-data.
+ */
+ public static final int USES_POLICY_DISABLE_KEYGUARD_FEATURES = 9;
+
+ /** @hide */
+ public static class PolicyInfo {
+ public final int ident;
+ public final String tag;
+ public final int label;
+ public final int description;
+ public final int labelForSecondaryUsers;
+ public final int descriptionForSecondaryUsers;
+
+ public PolicyInfo(int ident, String tag, int label, int description) {
+ this(ident, tag, label, description, label, description);
+ }
+
+ public PolicyInfo(int ident, String tag, int label, int description,
+ int labelForSecondaryUsers, int descriptionForSecondaryUsers) {
+ this.ident = ident;
+ this.tag = tag;
+ this.label = label;
+ this.description = description;
+ this.labelForSecondaryUsers = labelForSecondaryUsers;
+ this.descriptionForSecondaryUsers = descriptionForSecondaryUsers;
+ }
+ }
+
+ static ArrayList<PolicyInfo> sPoliciesDisplayOrder = new ArrayList<PolicyInfo>();
+ static HashMap<String, Integer> sKnownPolicies = new HashMap<String, Integer>();
+ static SparseArray<PolicyInfo> sRevKnownPolicies = new SparseArray<PolicyInfo>();
+
+ static {
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WIPE_DATA, "wipe-data",
+ com.android.internal.R.string.policylab_wipeData,
+ com.android.internal.R.string.policydesc_wipeData,
+ com.android.internal.R.string.policylab_wipeData_secondaryUser,
+ com.android.internal.R.string.policydesc_wipeData_secondaryUser
+ ));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_RESET_PASSWORD, "reset-password",
+ com.android.internal.R.string.policylab_resetPassword,
+ com.android.internal.R.string.policydesc_resetPassword));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_LIMIT_PASSWORD, "limit-password",
+ com.android.internal.R.string.policylab_limitPassword,
+ com.android.internal.R.string.policydesc_limitPassword));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login",
+ com.android.internal.R.string.policylab_watchLogin,
+ com.android.internal.R.string.policydesc_watchLogin,
+ com.android.internal.R.string.policylab_watchLogin,
+ com.android.internal.R.string.policydesc_watchLogin_secondaryUser
+ ));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock",
+ com.android.internal.R.string.policylab_forceLock,
+ com.android.internal.R.string.policydesc_forceLock));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_SETS_GLOBAL_PROXY, "set-global-proxy",
+ com.android.internal.R.string.policylab_setGlobalProxy,
+ com.android.internal.R.string.policydesc_setGlobalProxy));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_EXPIRE_PASSWORD, "expire-password",
+ com.android.internal.R.string.policylab_expirePassword,
+ com.android.internal.R.string.policydesc_expirePassword));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_ENCRYPTED_STORAGE, "encrypted-storage",
+ com.android.internal.R.string.policylab_encryptedStorage,
+ com.android.internal.R.string.policydesc_encryptedStorage));
+ sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_DISABLE_CAMERA, "disable-camera",
+ com.android.internal.R.string.policylab_disableCamera,
+ com.android.internal.R.string.policydesc_disableCamera));
+ sPoliciesDisplayOrder.add(new PolicyInfo(
+ USES_POLICY_DISABLE_KEYGUARD_FEATURES, "disable-keyguard-features",
+ com.android.internal.R.string.policylab_disableKeyguardFeatures,
+ com.android.internal.R.string.policydesc_disableKeyguardFeatures));
+
+ for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
+ PolicyInfo pi = sPoliciesDisplayOrder.get(i);
+ sRevKnownPolicies.put(pi.ident, pi);
+ sKnownPolicies.put(pi.tag, pi.ident);
+ }
+ }
+
+ /**
+ * The BroadcastReceiver that implements this device admin component.
+ */
+ final ActivityInfo mActivityInfo;
+
+ /**
+ * Whether this should be visible to the user.
+ */
+ boolean mVisible;
+
+ /**
+ * The policies this administrator needs access to.
+ */
+ int mUsesPolicies;
+
+
+ /**
+ * Constructor.
+ *
+ * @param context The Context in which we are parsing the device admin.
+ * @param resolveInfo The ResolveInfo returned from the package manager about
+ * this device admin's component.
+ */
+ public DeviceAdminInfo(Context context, ResolveInfo resolveInfo)
+ throws XmlPullParserException, IOException {
+ this(context, resolveInfo.activityInfo);
+ }
+ /**
+ * Constructor.
+ *
+ * @param context The Context in which we are parsing the device admin.
+ * @param activityInfo The ActivityInfo returned from the package manager about
+ * this device admin's component.
+ *
+ * @hide
+ */
+ public DeviceAdminInfo(Context context, ActivityInfo activityInfo)
+ throws XmlPullParserException, IOException {
+ mActivityInfo = activityInfo;
+
+ PackageManager pm = context.getPackageManager();
+
+ XmlResourceParser parser = null;
+ try {
+ parser = mActivityInfo.loadXmlMetaData(pm, DeviceAdminReceiver.DEVICE_ADMIN_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException("No "
+ + DeviceAdminReceiver.DEVICE_ADMIN_META_DATA + " meta-data");
+ }
+
+ Resources res = pm.getResourcesForApplication(mActivityInfo.applicationInfo);
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"device-admin".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with device-admin tag");
+ }
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.DeviceAdmin);
+
+ mVisible = sa.getBoolean(
+ com.android.internal.R.styleable.DeviceAdmin_visible, true);
+
+ sa.recycle();
+
+ int outerDepth = parser.getDepth();
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ String tagName = parser.getName();
+ if (tagName.equals("uses-policies")) {
+ int innerDepth = parser.getDepth();
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ String policyName = parser.getName();
+ Integer val = sKnownPolicies.get(policyName);
+ if (val != null) {
+ mUsesPolicies |= 1 << val.intValue();
+ } else {
+ Log.w(TAG, "Unknown tag under uses-policies of "
+ + getComponent() + ": " + policyName);
+ }
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ throw new XmlPullParserException(
+ "Unable to create context for: " + mActivityInfo.packageName);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ DeviceAdminInfo(Parcel source) {
+ mActivityInfo = ActivityInfo.CREATOR.createFromParcel(source);
+ mUsesPolicies = source.readInt();
+ }
+
+ /**
+ * Return the .apk package that implements this device admin.
+ */
+ public String getPackageName() {
+ return mActivityInfo.packageName;
+ }
+
+ /**
+ * Return the class name of the receiver component that implements
+ * this device admin.
+ */
+ public String getReceiverName() {
+ return mActivityInfo.name;
+ }
+
+ /**
+ * Return the raw information about the receiver implementing this
+ * device admin. Do not modify the returned object.
+ */
+ public ActivityInfo getActivityInfo() {
+ return mActivityInfo;
+ }
+
+ /**
+ * Return the component of the receiver that implements this device admin.
+ */
+ @NonNull
+ public ComponentName getComponent() {
+ return new ComponentName(mActivityInfo.packageName,
+ mActivityInfo.name);
+ }
+
+ /**
+ * Load the user-displayed label for this device admin.
+ *
+ * @param pm Supply a PackageManager used to load the device admin's
+ * resources.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ return mActivityInfo.loadLabel(pm);
+ }
+
+ /**
+ * Load user-visible description associated with this device admin.
+ *
+ * @param pm Supply a PackageManager used to load the device admin's
+ * resources.
+ */
+ public CharSequence loadDescription(PackageManager pm) throws NotFoundException {
+ if (mActivityInfo.descriptionRes != 0) {
+ return pm.getText(mActivityInfo.packageName,
+ mActivityInfo.descriptionRes, mActivityInfo.applicationInfo);
+ }
+ throw new NotFoundException();
+ }
+
+ /**
+ * Load the user-displayed icon for this device admin.
+ *
+ * @param pm Supply a PackageManager used to load the device admin's
+ * resources.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ return mActivityInfo.loadIcon(pm);
+ }
+
+ /**
+ * Returns whether this device admin would like to be visible to the
+ * user, even when it is not enabled.
+ */
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Return true if the device admin has requested that it be able to use
+ * the given policy control. The possible policy identifier inputs are:
+ * {@link #USES_POLICY_LIMIT_PASSWORD}, {@link #USES_POLICY_WATCH_LOGIN},
+ * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_FORCE_LOCK},
+ * {@link #USES_POLICY_WIPE_DATA},
+ * {@link #USES_POLICY_EXPIRE_PASSWORD}, {@link #USES_ENCRYPTED_STORAGE},
+ * {@link #USES_POLICY_DISABLE_CAMERA}.
+ */
+ public boolean usesPolicy(int policyIdent) {
+ return (mUsesPolicies & (1<<policyIdent)) != 0;
+ }
+
+ /**
+ * Return the XML tag name for the given policy identifier. Valid identifiers
+ * are as per {@link #usesPolicy(int)}. If the given identifier is not
+ * known, null is returned.
+ */
+ public String getTagForPolicy(int policyIdent) {
+ return sRevKnownPolicies.get(policyIdent).tag;
+ }
+
+ /** @hide */
+ public ArrayList<PolicyInfo> getUsedPolicies() {
+ ArrayList<PolicyInfo> res = new ArrayList<PolicyInfo>();
+ for (int i=0; i<sPoliciesDisplayOrder.size(); i++) {
+ PolicyInfo pi = sPoliciesDisplayOrder.get(i);
+ if (usesPolicy(pi.ident)) {
+ res.add(pi);
+ }
+ }
+ return res;
+ }
+
+ /** @hide */
+ public void writePoliciesToXml(XmlSerializer out)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ out.attribute(null, "flags", Integer.toString(mUsesPolicies));
+ }
+
+ /** @hide */
+ public void readPoliciesFromXml(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ mUsesPolicies = Integer.parseInt(
+ parser.getAttributeValue(null, "flags"));
+ }
+
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "Receiver:");
+ mActivityInfo.dump(pw, prefix + " ");
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceAdminInfo{" + mActivityInfo.name + "}";
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ mActivityInfo.writeToParcel(dest, flags);
+ dest.writeInt(mUsesPolicies);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<DeviceAdminInfo> CREATOR =
+ new Parcelable.Creator<DeviceAdminInfo>() {
+ public DeviceAdminInfo createFromParcel(Parcel source) {
+ return new DeviceAdminInfo(source);
+ }
+
+ public DeviceAdminInfo[] newArray(int size) {
+ return new DeviceAdminInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/app/admin/DeviceAdminReceiver.java b/android/app/admin/DeviceAdminReceiver.java
new file mode 100644
index 00000000..d0d98c9f
--- /dev/null
+++ b/android/app/admin/DeviceAdminReceiver.java
@@ -0,0 +1,926 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.accounts.AccountManager;
+import android.annotation.BroadcastBehavior;
+import android.annotation.IntDef;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserHandle;
+import android.security.KeyChain;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base class for implementing a device administration component. This
+ * class provides a convenience for interpreting the raw intent actions
+ * that are sent by the system.
+ *
+ * <p>The callback methods, like the base
+ * {@link BroadcastReceiver#onReceive(Context, Intent) BroadcastReceiver.onReceive()}
+ * method, happen on the main thread of the process. Thus long running
+ * operations must be done on another thread. Note that because a receiver
+ * is done once returning from its receive function, such long-running operations
+ * should probably be done in a {@link Service}.
+ *
+ * <p>When publishing your DeviceAdmin subclass as a receiver, it must
+ * handle {@link #ACTION_DEVICE_ADMIN_ENABLED} and require the
+ * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission. A typical
+ * manifest entry would look like:</p>
+ *
+ * {@sample development/samples/ApiDemos/AndroidManifest.xml device_admin_declaration}
+ *
+ * <p>The meta-data referenced here provides addition information specific
+ * to the device administrator, as parsed by the {@link DeviceAdminInfo} class.
+ * A typical file would be:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/xml/device_admin_sample.xml meta_data}
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about device administration, read the
+ * <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class DeviceAdminReceiver extends BroadcastReceiver {
+ private static String TAG = "DevicePolicy";
+ private static boolean localLOGV = false;
+
+ /**
+ * This is the primary action that a device administrator must implement to be
+ * allowed to manage a device. This will be set to the receiver
+ * when the user enables it for administration. You will generally
+ * handle this in {@link DeviceAdminReceiver#onEnabled(Context, Intent)}. To be
+ * supported, the receiver must also require the
+ * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_DEVICE_ADMIN_ENABLED
+ = "android.app.action.DEVICE_ADMIN_ENABLED";
+
+ /**
+ * Action sent to a device administrator when the user has requested to
+ * disable it, but before this has actually been done. This gives you
+ * a chance to supply a message to the user about the impact of
+ * disabling your admin, by setting the extra field
+ * {@link #EXTRA_DISABLE_WARNING} in the result Intent. If not set,
+ * no warning will be displayed. If set, the given text will be shown
+ * to the user before they disable your admin.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_DEVICE_ADMIN_DISABLE_REQUESTED
+ = "android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED";
+
+ /**
+ * A CharSequence that can be shown to the user informing them of the
+ * impact of disabling your admin.
+ *
+ * @see #ACTION_DEVICE_ADMIN_DISABLE_REQUESTED
+ */
+ public static final String EXTRA_DISABLE_WARNING = "android.app.extra.DISABLE_WARNING";
+
+ /**
+ * Action sent to a device administrator when the user has disabled
+ * it. Upon return, the application no longer has access to the
+ * protected device policy manager APIs. You will generally
+ * handle this in {@link DeviceAdminReceiver#onDisabled(Context, Intent)}. Note
+ * that this action will be
+ * sent the receiver regardless of whether it is explicitly listed in
+ * its intent filter.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_DEVICE_ADMIN_DISABLED
+ = "android.app.action.DEVICE_ADMIN_DISABLED";
+
+ /**
+ * Action sent to a device administrator when the user has changed the password of their device
+ * or profile challenge. You can at this point check the characteristics
+ * of the new password with {@link DevicePolicyManager#isActivePasswordSufficient()
+ * DevicePolicyManager.isActivePasswordSufficient()}.
+ * You will generally
+ * handle this in {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent, UserHandle)}.
+ *
+ * <p>The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to receive
+ * this broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_PASSWORD_CHANGED
+ = "android.app.action.ACTION_PASSWORD_CHANGED";
+
+ /**
+ * Action sent to a device administrator when the user has entered an incorrect device
+ * or profile challenge password. You can at this point check the
+ * number of failed password attempts there have been with
+ * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts
+ * DevicePolicyManager.getCurrentFailedPasswordAttempts()}. You will generally
+ * handle this in {@link DeviceAdminReceiver#onPasswordFailed(Context, Intent, UserHandle)}.
+ *
+ * <p>The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to receive
+ * this broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_PASSWORD_FAILED
+ = "android.app.action.ACTION_PASSWORD_FAILED";
+
+ /**
+ * Action sent to a device administrator when the user has successfully entered their device
+ * or profile challenge password, after failing one or more times. You will generally
+ * handle this in {@link DeviceAdminReceiver#onPasswordSucceeded(Context, Intent, UserHandle)}.
+ *
+ * <p>The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to receive
+ * this broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_PASSWORD_SUCCEEDED
+ = "android.app.action.ACTION_PASSWORD_SUCCEEDED";
+
+ /**
+ * Action periodically sent to a device administrator when the device or profile challenge
+ * password is expiring. You will generally
+ * handle this in {@link DeviceAdminReceiver#onPasswordExpiring(Context, Intent, UserHandle)}.
+ *
+ * <p>The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_EXPIRE_PASSWORD} to receive
+ * this broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_PASSWORD_EXPIRING
+ = "android.app.action.ACTION_PASSWORD_EXPIRING";
+
+ /**
+ * Action sent to a device administrator to notify that the device is entering
+ * lock task mode. The extra {@link #EXTRA_LOCK_TASK_PACKAGE}
+ * will describe the package using lock task mode.
+ *
+ * <p>The calling device admin must be the device owner or profile
+ * owner to receive this broadcast.
+ *
+ * @see DevicePolicyManager#isLockTaskPermitted(String)
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_LOCK_TASK_ENTERING
+ = "android.app.action.LOCK_TASK_ENTERING";
+
+ /**
+ * Action sent to a device administrator to notify that the device is exiting
+ * lock task mode.
+ *
+ * <p>The calling device admin must be the device owner or profile
+ * owner to receive this broadcast.
+ *
+ * @see DevicePolicyManager#isLockTaskPermitted(String)
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_LOCK_TASK_EXITING
+ = "android.app.action.LOCK_TASK_EXITING";
+
+ /**
+ * A string containing the name of the package entering lock task mode.
+ *
+ * @see #ACTION_LOCK_TASK_ENTERING
+ */
+ public static final String EXTRA_LOCK_TASK_PACKAGE =
+ "android.app.extra.LOCK_TASK_PACKAGE";
+
+ /**
+ * Broadcast Action: This broadcast is sent to indicate that provisioning of a managed profile
+ * or managed device has completed successfully.
+ *
+ * <p>The broadcast is limited to the profile that will be managed by the application that
+ * requested provisioning. In the device owner case the profile is the primary user.
+ * The broadcast will also be limited to the {@link DeviceAdminReceiver} component
+ * specified in the original intent or NFC bump that started the provisioning process
+ * (see {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE
+ * DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE}).
+ *
+ * <p>A device admin application which listens to this intent can find out if the device was
+ * provisioned for the device owner or profile owner case by calling respectively
+ * {@link android.app.admin.DevicePolicyManager#isDeviceOwnerApp} and
+ * {@link android.app.admin.DevicePolicyManager#isProfileOwnerApp}. You will generally handle
+ * this in {@link DeviceAdminReceiver#onProfileProvisioningComplete}.
+ *
+ * @see DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_PROFILE_PROVISIONING_COMPLETE =
+ "android.app.action.PROFILE_PROVISIONING_COMPLETE";
+
+ /**
+ * Action sent to a device administrator to notify that the device user
+ * has declined sharing a bugreport.
+ *
+ * <p>The calling device admin must be the device owner to receive this broadcast.
+ * @see DevicePolicyManager#requestBugreport
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_BUGREPORT_SHARING_DECLINED =
+ "android.app.action.BUGREPORT_SHARING_DECLINED";
+
+ /**
+ * Action sent to a device administrator to notify that the collection of a bugreport
+ * has failed.
+ *
+ * <p>The calling device admin must be the device owner to receive this broadcast.
+ * @see DevicePolicyManager#requestBugreport
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_BUGREPORT_FAILED = "android.app.action.BUGREPORT_FAILED";
+
+ /**
+ * Action sent to a device administrator to share the bugreport.
+ *
+ * <p>The calling device admin must be the device owner to receive this broadcast.
+ * @see DevicePolicyManager#requestBugreport
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_BUGREPORT_SHARE =
+ "android.app.action.BUGREPORT_SHARE";
+
+ /**
+ * Broadcast action: notify that a new batch of security logs is ready to be collected.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_SECURITY_LOGS_AVAILABLE
+ = "android.app.action.SECURITY_LOGS_AVAILABLE";
+
+ /**
+ * Broadcast action: notify that a new batch of network logs is ready to be collected.
+ * @see DeviceAdminReceiver#onNetworkLogsAvailable
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_NETWORK_LOGS_AVAILABLE
+ = "android.app.action.NETWORK_LOGS_AVAILABLE";
+
+ /**
+ * A {@code long} containing a token of the current batch of network logs, that has to be used
+ * to retrieve the batch of logs by the device owner.
+ *
+ * @see #ACTION_NETWORK_LOGS_AVAILABLE
+ * @see DevicePolicyManager#retrieveNetworkLogs
+ * @hide
+ */
+ public static final String EXTRA_NETWORK_LOGS_TOKEN =
+ "android.app.extra.EXTRA_NETWORK_LOGS_TOKEN";
+
+ /**
+ * An {@code int} count representing a total count of network logs inside the current batch of
+ * network logs.
+ *
+ * @see #ACTION_NETWORK_LOGS_AVAILABLE
+ * @hide
+ */
+ public static final String EXTRA_NETWORK_LOGS_COUNT =
+ "android.app.extra.EXTRA_NETWORK_LOGS_COUNT";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been added.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the new user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_ADDED = "android.app.action.USER_ADDED";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been removed.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the new user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_REMOVED = "android.app.action.USER_REMOVED";
+
+ /**
+ * A string containing the SHA-256 hash of the bugreport file.
+ *
+ * @see #ACTION_BUGREPORT_SHARE
+ * @hide
+ */
+ public static final String EXTRA_BUGREPORT_HASH = "android.app.extra.BUGREPORT_HASH";
+
+ /**
+ * An {@code int} failure code representing the reason of the bugreport failure. One of
+ * {@link #BUGREPORT_FAILURE_FAILED_COMPLETING}
+ * or {@link #BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE}
+ *
+ * @see #ACTION_BUGREPORT_FAILED
+ * @hide
+ */
+ public static final String EXTRA_BUGREPORT_FAILURE_REASON =
+ "android.app.extra.BUGREPORT_FAILURE_REASON";
+
+ /**
+ * An interface representing reason of bugreport failure.
+ *
+ * @see #EXTRA_BUGREPORT_FAILURE_REASON
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BUGREPORT_FAILURE_FAILED_COMPLETING,
+ BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE
+ })
+ public @interface BugreportFailureCode {}
+
+ /**
+ * Bugreport completion process failed.
+ *
+ * <p>If this error code is received, the requesting of bugreport can be retried.
+ * @see DevicePolicyManager#requestBugreport
+ */
+ public static final int BUGREPORT_FAILURE_FAILED_COMPLETING = 0;
+
+ /**
+ * Bugreport has been created, but is no longer available for collection.
+ *
+ * <p>This error likely occurs because the user of the device hasn't consented to share
+ * the bugreport for a long period after its creation.
+ *
+ * <p>If this error code is received, the requesting of bugreport can be retried.
+ * @see DevicePolicyManager#requestBugreport
+ */
+ public static final int BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE = 1;
+
+ /** @hide */
+ public static final String ACTION_CHOOSE_PRIVATE_KEY_ALIAS =
+ "android.app.action.CHOOSE_PRIVATE_KEY_ALIAS";
+
+ /** @hide */
+ public static final String EXTRA_CHOOSE_PRIVATE_KEY_SENDER_UID =
+ "android.app.extra.CHOOSE_PRIVATE_KEY_SENDER_UID";
+
+ /** @hide */
+ public static final String EXTRA_CHOOSE_PRIVATE_KEY_URI =
+ "android.app.extra.CHOOSE_PRIVATE_KEY_URI";
+
+ /** @hide */
+ public static final String EXTRA_CHOOSE_PRIVATE_KEY_ALIAS =
+ "android.app.extra.CHOOSE_PRIVATE_KEY_ALIAS";
+
+ /** @hide */
+ public static final String EXTRA_CHOOSE_PRIVATE_KEY_RESPONSE =
+ "android.app.extra.CHOOSE_PRIVATE_KEY_RESPONSE";
+
+ /**
+ * Broadcast action: notify device owner that there is a pending system update.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_NOTIFY_PENDING_SYSTEM_UPDATE =
+ "android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE";
+
+ /**
+ * A long type extra for {@link #onSystemUpdatePending} recording the system time as given by
+ * {@link System#currentTimeMillis()} when the current pending system update is first available.
+ * @hide
+ */
+ public static final String EXTRA_SYSTEM_UPDATE_RECEIVED_TIME =
+ "android.app.extra.SYSTEM_UPDATE_RECEIVED_TIME";
+
+ /**
+ * Name under which a DevicePolicy component publishes information
+ * about itself. This meta-data must reference an XML resource containing
+ * a device-admin tag.
+ */
+ // TO DO: describe syntax.
+ public static final String DEVICE_ADMIN_META_DATA = "android.app.device_admin";
+
+ private DevicePolicyManager mManager;
+ private ComponentName mWho;
+
+ /**
+ * Retrieve the DevicePolicyManager interface for this administrator to work
+ * with the system.
+ */
+ public DevicePolicyManager getManager(Context context) {
+ if (mManager != null) {
+ return mManager;
+ }
+ mManager = (DevicePolicyManager)context.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ return mManager;
+ }
+
+ /**
+ * Retrieve the ComponentName describing who this device administrator is, for
+ * use in {@link DevicePolicyManager} APIs that require the administrator to
+ * identify itself.
+ */
+ public ComponentName getWho(Context context) {
+ if (mWho != null) {
+ return mWho;
+ }
+ mWho = new ComponentName(context, getClass());
+ return mWho;
+ }
+
+ /**
+ * Called after the administrator is first enabled, as a result of
+ * receiving {@link #ACTION_DEVICE_ADMIN_ENABLED}. At this point you
+ * can use {@link DevicePolicyManager} to set your desired policies.
+ *
+ * <p> If the admin is activated by a device owner, then the intent
+ * may contain private extras that are relevant to user setup.
+ * {@see DevicePolicyManager#createAndManageUser(ComponentName, String, ComponentName,
+ * PersistableBundle, int)}
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ */
+ public void onEnabled(Context context, Intent intent) {
+ }
+
+ /**
+ * Called when the user has asked to disable the administrator, as a result of
+ * receiving {@link #ACTION_DEVICE_ADMIN_DISABLE_REQUESTED}, giving you
+ * a chance to present a warning message to them. The message is returned
+ * as the result; if null is returned (the default implementation), no
+ * message will be displayed.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @return Return the warning message to display to the user before
+ * being disabled; if null is returned, no message is displayed.
+ */
+ public CharSequence onDisableRequested(Context context, Intent intent) {
+ return null;
+ }
+
+ /**
+ * Called prior to the administrator being disabled, as a result of
+ * receiving {@link #ACTION_DEVICE_ADMIN_DISABLED}. Upon return, you
+ * can no longer use the protected parts of the {@link DevicePolicyManager}
+ * API.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ */
+ public void onDisabled(Context context, Intent intent) {
+ }
+
+ /**
+ * Called after the user has changed their device or profile challenge password, as a result of
+ * receiving {@link #ACTION_PASSWORD_CHANGED}. At this point you
+ * can use {@link DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+ * to retrieve the active password characteristics.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}, use
+ * {@link #onPasswordChanged(Context, Intent, UserHandle)} instead.
+ */
+ @Deprecated
+ public void onPasswordChanged(Context context, Intent intent) {
+ }
+
+ /**
+ * Called after the user has changed their device or profile challenge password, as a result of
+ * receiving {@link #ACTION_PASSWORD_CHANGED}. At this point you
+ * can use {@link DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+ * to retrieve the active password characteristics.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param user The user or profile for whom the password changed. To see whether this
+ * user is the current profile or a parent user, check for equality with
+ * {@link Process#myUserHandle}.
+ */
+ public void onPasswordChanged(Context context, Intent intent, UserHandle user) {
+ onPasswordChanged(context, intent);
+ }
+
+ /**
+ * Called after the user has failed at entering their device or profile challenge password,
+ * as a result of receiving {@link #ACTION_PASSWORD_FAILED}. At this point you can use
+ * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts()} to retrieve the number of
+ * failed password attempts.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}, use
+ * {@link #onPasswordFailed(Context, Intent, UserHandle)} instead.
+ */
+ @Deprecated
+ public void onPasswordFailed(Context context, Intent intent) {
+ }
+
+ /**
+ * Called after the user has failed at entering their device or profile challenge password,
+ * as a result of receiving {@link #ACTION_PASSWORD_FAILED}. At this point you can use
+ * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts()} to retrieve the number of
+ * failed password attempts.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param user The user or profile for whom the password check failed. To see whether this
+ * user is the current profile or a parent user, check for equality with
+ * {@link Process#myUserHandle}.
+ */
+ public void onPasswordFailed(Context context, Intent intent, UserHandle user) {
+ onPasswordFailed(context, intent);
+ }
+
+ /**
+ * Called after the user has succeeded at entering their device or profile challenge password,
+ * as a result of receiving {@link #ACTION_PASSWORD_SUCCEEDED}. This will
+ * only be received the first time they succeed after having previously
+ * failed.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}, use
+ * {@link #onPasswordSucceeded(Context, Intent, UserHandle)} instead.
+ */
+ @Deprecated
+ public void onPasswordSucceeded(Context context, Intent intent) {
+ }
+
+ /**
+ * Called after the user has succeeded at entering their device or profile challenge password,
+ * as a result of receiving {@link #ACTION_PASSWORD_SUCCEEDED}. This will
+ * only be received the first time they succeed after having previously
+ * failed.
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param user The user of profile for whom the password check succeeded. To see whether this
+ * user is the current profile or a parent user, check for equality with
+ * {@link Process#myUserHandle}.
+ */
+ public void onPasswordSucceeded(Context context, Intent intent, UserHandle user) {
+ onPasswordSucceeded(context, intent);
+ }
+
+ /**
+ * Called periodically when the device or profile challenge password is about to expire
+ * or has expired. It will typically be called at these times: on device boot, once per day
+ * before the password expires, and at the time when the password expires.
+ *
+ * <p>If the password is not updated by the user, this method will continue to be called
+ * once per day until the password is changed or the device admin disables password expiration.
+ *
+ * <p>The admin will typically post a notification requesting the user to change their password
+ * in response to this call. The actual password expiration time can be obtained by calling
+ * {@link DevicePolicyManager#getPasswordExpiration(ComponentName) }
+ *
+ * <p>The admin should be sure to take down any notifications it posted in response to this call
+ * when it receives {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent) }.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}, use
+ * {@link #onPasswordExpiring(Context, Intent, UserHandle)} instead.
+ */
+ @Deprecated
+ public void onPasswordExpiring(Context context, Intent intent) {
+ }
+
+ /**
+ * Called periodically when the device or profile challenge password is about to expire
+ * or has expired. It will typically be called at these times: on device boot, once per day
+ * before the password expires, and at the time when the password expires.
+ *
+ * <p>If the password is not updated by the user, this method will continue to be called
+ * once per day until the password is changed or the device admin disables password expiration.
+ *
+ * <p>The admin will typically post a notification requesting the user to change their password
+ * in response to this call. The actual password expiration time can be obtained by calling
+ * {@link DevicePolicyManager#getPasswordExpiration(ComponentName) }
+ *
+ * <p>The admin should be sure to take down any notifications it posted in response to this call
+ * when it receives {@link DeviceAdminReceiver#onPasswordChanged(Context, Intent, UserHandle) }.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param user The user or profile for whom the password is expiring. To see whether this
+ * user is the current profile or a parent user, check for equality with
+ * {@link Process#myUserHandle}.
+ */
+ public void onPasswordExpiring(Context context, Intent intent, UserHandle user) {
+ onPasswordExpiring(context, intent);
+ }
+
+ /**
+ * Called when provisioning of a managed profile or managed device has completed successfully.
+ *
+ * <p> As a prerequisite for the execution of this callback the {@link DeviceAdminReceiver} has
+ * to declare an intent filter for {@link #ACTION_PROFILE_PROVISIONING_COMPLETE}.
+ * Its component must also be specified in the {@link DevicePolicyManager#EXTRA_DEVICE_ADMIN}
+ * of the {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} intent that started the
+ * managed provisioning.
+ *
+ * <p>When provisioning of a managed profile is complete, the managed profile is hidden until
+ * the profile owner calls {@link DevicePolicyManager#setProfileEnabled(ComponentName admin)}.
+ * Typically a profile owner will enable the profile when it has finished any additional setup
+ * such as adding an account by using the {@link AccountManager} and calling APIs to bring the
+ * profile into the desired state.
+ *
+ * <p> Note that provisioning completes without waiting for any server interactions, so the
+ * profile owner needs to wait for data to be available if required (e.g. Android device IDs or
+ * other data that is set as a result of server interactions).
+ *
+ * <p>From version {@link android.os.Build.VERSION_CODES#O}, when managed provisioning has
+ * completed, along with this callback the activity intent
+ * {@link DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the same
+ * application.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ */
+ public void onProfileProvisioningComplete(Context context, Intent intent) {
+ }
+
+ /**
+ * Called during provisioning of a managed device to allow the device initializer to perform
+ * user setup steps.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @deprecated Do not use
+ */
+ @Deprecated
+ @SystemApi
+ public void onReadyForUserInitialization(Context context, Intent intent) {
+ }
+
+ /**
+ * Called when a device is entering lock task mode.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param pkg If entering, the authorized package using lock task mode, otherwise null.
+ */
+ public void onLockTaskModeEntering(Context context, Intent intent, String pkg) {
+ }
+
+ /**
+ * Called when a device is exiting lock task mode.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ */
+ public void onLockTaskModeExiting(Context context, Intent intent) {
+ }
+
+ /**
+ * Allows this receiver to select the alias for a private key and certificate pair for
+ * authentication. If this method returns null, the default {@link android.app.Activity} will be
+ * shown that lets the user pick a private key and certificate pair.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param uid The uid asking for the private key and certificate pair.
+ * @param uri The URI to authenticate, may be null.
+ * @param alias The alias preselected by the client, or null.
+ * @return The private key alias to return and grant access to.
+ * @see KeyChain#choosePrivateKeyAlias
+ */
+ public String onChoosePrivateKeyAlias(Context context, Intent intent, int uid, Uri uri,
+ String alias) {
+ return null;
+ }
+
+ /**
+ * Called when the information about a pending system update is available.
+ *
+ * <p>Allows the receiver to be notified when information about a pending system update is
+ * available from the system update service. The same pending system update can trigger multiple
+ * calls to this method, so it is necessary to examine the incoming parameters for details about
+ * the update.
+ *
+ * <p>This callback is only applicable to device owners and profile owners.
+ *
+ * <p>To get further information about a pending system update (for example, whether or not the
+ * update is a security patch), the device owner or profile owner can call
+ * {@link DevicePolicyManager#getPendingSystemUpdate}.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param receivedTime The time as given by {@link System#currentTimeMillis()} indicating when
+ * the current pending update was first available. -1 if no pending update is available.
+ * @see DevicePolicyManager#getPendingSystemUpdate
+ */
+ public void onSystemUpdatePending(Context context, Intent intent, long receivedTime) {
+ }
+
+ /**
+ * Called when sharing a bugreport has been cancelled by the user of the device.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @see DevicePolicyManager#requestBugreport
+ */
+ public void onBugreportSharingDeclined(Context context, Intent intent) {
+ }
+
+ /**
+ * Called when the bugreport has been shared with the device administrator app.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}. Contains the URI of
+ * the bugreport file (with MIME type "application/vnd.android.bugreport"), that can be accessed
+ * by calling {@link Intent#getData()}
+ * @param bugreportHash SHA-256 hash of the bugreport file.
+ * @see DevicePolicyManager#requestBugreport
+ */
+ public void onBugreportShared(Context context, Intent intent, String bugreportHash) {
+ }
+
+ /**
+ * Called when the bugreport collection flow has failed.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param failureCode int containing failure code. One of
+ * {@link #BUGREPORT_FAILURE_FAILED_COMPLETING}
+ * or {@link #BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE}
+ * @see DevicePolicyManager#requestBugreport
+ */
+ public void onBugreportFailed(Context context, Intent intent,
+ @BugreportFailureCode int failureCode) {
+ }
+
+ /**
+ * Called when a new batch of security logs can be retrieved.
+ *
+ * <p>If a secondary user or profile is created, this callback won't be received until all users
+ * become affiliated again (even if security logging is enabled).
+ * See {@link DevicePolicyManager#setAffiliationIds}
+ *
+ * <p>This callback will be re-triggered if the logs are not retrieved.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @see DevicePolicyManager#retrieveSecurityLogs(ComponentName)
+ */
+ public void onSecurityLogsAvailable(Context context, Intent intent) {
+ }
+
+ /**
+ * Called each time a new batch of network logs can be retrieved. This callback method will only
+ * ever be called when network logging is enabled. The logs can only be retrieved while network
+ * logging is enabled.
+ *
+ * <p>If a secondary user or profile is created, this callback won't be received until all users
+ * become affiliated again (even if network logging is enabled). It will also no longer be
+ * possible to retrieve the network logs batch with the most recent {@code batchToken} provided
+ * by this callback. See {@link DevicePolicyManager#setAffiliationIds}.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param batchToken The token representing the current batch of network logs.
+ * @param networkLogsCount The total count of events in the current batch of network logs.
+ * @see DevicePolicyManager#retrieveNetworkLogs
+ */
+ public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+ int networkLogsCount) {
+ }
+
+ /**
+ * Called when a user or profile is created.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param newUser The {@link UserHandle} of the user that has just been added.
+ */
+ public void onUserAdded(Context context, Intent intent, UserHandle newUser) {
+ }
+
+ /**
+ * Called when a user or profile is removed.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param removedUser The {@link UserHandle} of the user that has just been removed.
+ */
+ public void onUserRemoved(Context context, Intent intent, UserHandle removedUser) {
+ }
+
+ /**
+ * Intercept standard device administrator broadcasts. Implementations
+ * should not override this method; it is better to implement the
+ * convenience callbacks for each action.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (ACTION_PASSWORD_CHANGED.equals(action)) {
+ onPasswordChanged(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_PASSWORD_FAILED.equals(action)) {
+ onPasswordFailed(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_PASSWORD_SUCCEEDED.equals(action)) {
+ onPasswordSucceeded(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_DEVICE_ADMIN_ENABLED.equals(action)) {
+ onEnabled(context, intent);
+ } else if (ACTION_DEVICE_ADMIN_DISABLE_REQUESTED.equals(action)) {
+ CharSequence res = onDisableRequested(context, intent);
+ if (res != null) {
+ Bundle extras = getResultExtras(true);
+ extras.putCharSequence(EXTRA_DISABLE_WARNING, res);
+ }
+ } else if (ACTION_DEVICE_ADMIN_DISABLED.equals(action)) {
+ onDisabled(context, intent);
+ } else if (ACTION_PASSWORD_EXPIRING.equals(action)) {
+ onPasswordExpiring(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_PROFILE_PROVISIONING_COMPLETE.equals(action)) {
+ onProfileProvisioningComplete(context, intent);
+ } else if (ACTION_CHOOSE_PRIVATE_KEY_ALIAS.equals(action)) {
+ int uid = intent.getIntExtra(EXTRA_CHOOSE_PRIVATE_KEY_SENDER_UID, -1);
+ Uri uri = intent.getParcelableExtra(EXTRA_CHOOSE_PRIVATE_KEY_URI);
+ String alias = intent.getStringExtra(EXTRA_CHOOSE_PRIVATE_KEY_ALIAS);
+ String chosenAlias = onChoosePrivateKeyAlias(context, intent, uid, uri, alias);
+ setResultData(chosenAlias);
+ } else if (ACTION_LOCK_TASK_ENTERING.equals(action)) {
+ String pkg = intent.getStringExtra(EXTRA_LOCK_TASK_PACKAGE);
+ onLockTaskModeEntering(context, intent, pkg);
+ } else if (ACTION_LOCK_TASK_EXITING.equals(action)) {
+ onLockTaskModeExiting(context, intent);
+ } else if (ACTION_NOTIFY_PENDING_SYSTEM_UPDATE.equals(action)) {
+ long receivedTime = intent.getLongExtra(EXTRA_SYSTEM_UPDATE_RECEIVED_TIME, -1);
+ onSystemUpdatePending(context, intent, receivedTime);
+ } else if (ACTION_BUGREPORT_SHARING_DECLINED.equals(action)) {
+ onBugreportSharingDeclined(context, intent);
+ } else if (ACTION_BUGREPORT_SHARE.equals(action)) {
+ String bugreportFileHash = intent.getStringExtra(EXTRA_BUGREPORT_HASH);
+ onBugreportShared(context, intent, bugreportFileHash);
+ } else if (ACTION_BUGREPORT_FAILED.equals(action)) {
+ int failureCode = intent.getIntExtra(EXTRA_BUGREPORT_FAILURE_REASON,
+ BUGREPORT_FAILURE_FAILED_COMPLETING);
+ onBugreportFailed(context, intent, failureCode);
+ } else if (ACTION_SECURITY_LOGS_AVAILABLE.equals(action)) {
+ onSecurityLogsAvailable(context, intent);
+ } else if (ACTION_NETWORK_LOGS_AVAILABLE.equals(action)) {
+ long batchToken = intent.getLongExtra(EXTRA_NETWORK_LOGS_TOKEN, -1);
+ int networkLogsCount = intent.getIntExtra(EXTRA_NETWORK_LOGS_COUNT, 0);
+ onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
+ } else if (ACTION_USER_ADDED.equals(action)) {
+ onUserAdded(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_REMOVED.equals(action)) {
+ onUserRemoved(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ }
+ }
+}
diff --git a/android/app/admin/DeviceAdminService.java b/android/app/admin/DeviceAdminService.java
new file mode 100644
index 00000000..04fff049
--- /dev/null
+++ b/android/app/admin/DeviceAdminService.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.admin;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Base class for a service that device owner/profile owners can optionally have.
+ *
+ * <p>The system searches for it with an intent filter with the
+ * {@link DevicePolicyManager#ACTION_DEVICE_ADMIN_SERVICE} action, and tries to keep a bound
+ * connection as long as the hosting user is running, so that the device/profile owner is always
+ * considered to be in the foreground. This is useful to receive implicit broadcasts that
+ * can no longer be received by manifest receivers by apps targeting Android version
+ * {@link android.os.Build.VERSION_CODES#O}. Device/profile owners can use a runtime-registered
+ * broadcast receiver instead, and have a {@link DeviceAdminService} so that the process is always
+ * running.
+ *
+ * <p>Device/profile owners can use
+ * {@link android.content.pm.PackageManager#setComponentEnabledSetting(ComponentName, int, int)}
+ * to disable/enable its own service. For example, when a device/profile owner no longer needs
+ * to be in the foreground, it can (and should) disable its service.
+ *
+ * <p>The service must be protected with the permission
+ * {@link android.Manifest.permission#BIND_DEVICE_ADMIN}. Otherwise the system would ignore it.
+ *
+ * <p>When the owner process crashes, the service will be re-bound automatically after a
+ * back-off.
+ *
+ * <p>Note the process may still be killed if the system is under heavy memory pressure, in which
+ * case the process will be re-started later.
+ */
+public class DeviceAdminService extends Service {
+ private final IDeviceAdminServiceImpl mImpl;
+
+ public DeviceAdminService() {
+ mImpl = new IDeviceAdminServiceImpl();
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return mImpl.asBinder();
+ }
+
+ private class IDeviceAdminServiceImpl extends IDeviceAdminService.Stub {
+ }
+
+ // So far, we have no methods in this class.
+}
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
new file mode 100644
index 00000000..6bccad9f
--- /dev/null
+++ b/android/app/admin/DevicePolicyManager.java
@@ -0,0 +1,8185 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.annotation.ColorInt;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.app.IServiceConnection;
+import android.app.KeyguardManager;
+import android.app.admin.SecurityLog.SecurityEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
+import android.graphics.Bitmap;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.ContactsContract.Directory;
+import android.security.Credentials;
+import android.service.restrictions.RestrictionsReceiver;
+import android.telephony.TelephonyManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.org.conscrypt.TrustedCertificateStore;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Public interface for managing policies enforced on a device. Most clients of this class must be
+ * registered with the system as a <a href="{@docRoot}guide/topics/admin/device-admin.html">device
+ * administrator</a>. Additionally, a device administrator may be registered as either a profile or
+ * device owner. A given method is accessible to all device administrators unless the documentation
+ * for that method specifies that it is restricted to either device or profile owners. Any
+ * application calling an api may only pass as an argument a device administrator component it
+ * owns. Otherwise, a {@link SecurityException} will be thrown.
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about managing policies for device administration, read the <a href=
+ * "{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a> developer
+ * guide. </div>
+ */
+@SystemService(Context.DEVICE_POLICY_SERVICE)
+public class DevicePolicyManager {
+ private static String TAG = "DevicePolicyManager";
+
+ private final Context mContext;
+ private final IDevicePolicyManager mService;
+ private final boolean mParentInstance;
+
+ /** @hide */
+ public DevicePolicyManager(Context context, IDevicePolicyManager service) {
+ this(context, service, false);
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ protected DevicePolicyManager(Context context, IDevicePolicyManager service,
+ boolean parentInstance) {
+ mContext = context;
+ mService = service;
+ mParentInstance = parentInstance;
+ }
+
+ /** @hide test will override it. */
+ @VisibleForTesting
+ protected int myUserId() {
+ return UserHandle.myUserId();
+ }
+
+ /**
+ * Activity action: Starts the provisioning flow which sets up a managed profile.
+ *
+ * <p>A managed profile allows data separation for example for the usage of a
+ * device as a personal and corporate device. The user which provisioning is started from and
+ * the managed profile share a launcher.
+ *
+ * <p>This intent will typically be sent by a mobile device management application (MDM).
+ * Provisioning adds a managed profile and sets the MDM as the profile owner who has full
+ * control over the profile.
+ *
+ * <p>It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
+ *
+ * <p>In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this intent must contain the
+ * extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}.
+ * As of {@link android.os.Build.VERSION_CODES#M}, it should contain the extra
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead, although specifying only
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is still supported.
+ *
+ * <p>The intent may also contain the following extras:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}, optional </li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional, supported from
+ * {@link android.os.Build.VERSION_CODES#N}</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_MAIN_COLOR}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_USER_CONSENT}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DISCLAIMERS}, optional</li>
+ * </ul>
+ *
+ * <p>When managed provisioning has completed, broadcasts are sent to the application specified
+ * in the provisioning intent. The
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast is sent in the
+ * managed profile and the {@link #ACTION_MANAGED_PROFILE_PROVISIONED} broadcast is sent in
+ * the primary profile.
+ *
+ * <p>From version {@link android.os.Build.VERSION_CODES#O}, when managed provisioning has
+ * completed, along with the above broadcast, activity intent
+ * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the profile owner.
+ *
+ * <p>If provisioning fails, the managedProfile is removed so the device returns to its
+ * previous state.
+ *
+ * <p>If launched with {@link android.app.Activity#startActivityForResult(Intent, int)} a
+ * result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part of
+ * the provisioning flow was successful, although this doesn't guarantee the full flow will
+ * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies
+ * that the user backed-out of provisioning, or some precondition for provisioning wasn't met.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_MANAGED_PROFILE
+ = "android.app.action.PROVISION_MANAGED_PROFILE";
+
+ /**
+ * Activity action: Starts the provisioning flow which sets up a managed user.
+ *
+ * <p>This intent will typically be sent by a mobile device management application (MDM).
+ * Provisioning configures the user as managed user and sets the MDM as the profile
+ * owner who has full control over the user. Provisioning can only happen before user setup has
+ * been completed. Use {@link #isProvisioningAllowed(String)} to check if provisioning is
+ * allowed.
+ *
+ * <p>The intent contains the following extras:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_MAIN_COLOR}, optional</li>
+ * </ul>
+ *
+ * <p>If provisioning fails, the device returns to its previous state.
+ *
+ * <p>If launched with {@link android.app.Activity#startActivityForResult(Intent, int)} a
+ * result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part of
+ * the provisioning flow was successful, although this doesn't guarantee the full flow will
+ * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies
+ * that the user backed-out of provisioning, or some precondition for provisioning wasn't met.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_MANAGED_USER
+ = "android.app.action.PROVISION_MANAGED_USER";
+
+ /**
+ * Activity action: Starts the provisioning flow which sets up a managed device.
+ * Must be started with {@link android.app.Activity#startActivityForResult(Intent, int)}.
+ *
+ * <p> During device owner provisioning a device admin app is set as the owner of the device.
+ * A device owner has full control over the device. The device owner can not be modified by the
+ * user.
+ *
+ * <p> A typical use case would be a device that is owned by a company, but used by either an
+ * employee or client.
+ *
+ * <p> An intent with this action can be sent only on an unprovisioned device.
+ * It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
+ *
+ * <p>The intent contains the following extras:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_MAIN_COLOR}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DISCLAIMERS}, optional</li>
+ * </ul>
+ *
+ * <p>When device owner provisioning has completed, an intent of the type
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcast to the
+ * device owner.
+ *
+ * <p>From version {@link android.os.Build.VERSION_CODES#O}, when device owner provisioning has
+ * completed, along with the above broadcast, activity intent
+ * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the device owner.
+ *
+ * <p>If provisioning fails, the device is factory reset.
+ *
+ * <p>A result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part
+ * of the provisioning flow was successful, although this doesn't guarantee the full flow will
+ * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies
+ * that the user backed-out of provisioning, or some precondition for provisioning wasn't met.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_MANAGED_DEVICE
+ = "android.app.action.PROVISION_MANAGED_DEVICE";
+
+ /**
+ * Activity action: launch when user provisioning completed, i.e.
+ * {@link #getUserProvisioningState()} returns one of the complete state.
+ *
+ * <p> Please note that the API behavior is not necessarily consistent across various releases,
+ * and devices, as it's contract between SetupWizard and ManagedProvisioning. The default
+ * implementation is that ManagedProvisioning launches SetupWizard in NFC provisioning only.
+ *
+ * <p> The activity must be protected by permission
+ * {@link android.Manifest.permission#BIND_DEVICE_ADMIN}, and the process must hold
+ * {@link android.Manifest.permission#DISPATCH_PROVISIONING_MESSAGE} to be launched.
+ * Only one {@link ComponentName} in the entire system should be enabled, and the rest of the
+ * components are not started by this intent.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_STATE_USER_SETUP_COMPLETE =
+ "android.app.action.STATE_USER_SETUP_COMPLETE";
+
+ /**
+ * Activity action: Starts the provisioning flow which sets up a managed device.
+ *
+ * <p>During device owner provisioning, a device admin app is downloaded and set as the owner of
+ * the device. A device owner has full control over the device. The device owner can not be
+ * modified by the user and the only way of resetting the device is via factory reset.
+ *
+ * <p>A typical use case would be a device that is owned by a company, but used by either an
+ * employee or client.
+ *
+ * <p>The provisioning message should be sent to an unprovisioned device.
+ *
+ * <p>Unlike {@link #ACTION_PROVISION_MANAGED_DEVICE}, the provisioning message can only be sent
+ * by a privileged app with the permission
+ * {@link android.Manifest.permission#DISPATCH_PROVISIONING_MESSAGE}.
+ *
+ * <p>The provisioning intent contains the following properties:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_LABEL}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_ICON_URI}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOCAL_TIME} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_TIME_ZONE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOCALE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_SSID}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_HIDDEN} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_SECURITY_TYPE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PASSWORD}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_HOST}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_PORT} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_BYPASS}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PAC_URL}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_SUPPORT_URL}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ORGANIZATION_NAME}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li></ul>
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE =
+ "android.app.action.PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE";
+
+ /**
+ * Activity action: Starts the provisioning flow which sets up a managed device.
+ * Must be started with {@link android.app.Activity#startActivityForResult(Intent, int)}.
+ *
+ * <p>NOTE: This is only supported on split system user devices, and puts the device into a
+ * management state that is distinct from that reached by
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE} - specifically the device owner runs on the system
+ * user, and only has control over device-wide policies, not individual users and their data.
+ * The primary benefit is that multiple non-system users are supported when provisioning using
+ * this form of device management.
+ *
+ * <p>During device owner provisioning a device admin app is set as the owner of the device.
+ * A device owner has full control over the device. The device owner can not be modified by the
+ * user.
+ *
+ * <p>A typical use case would be a device that is owned by a company, but used by either an
+ * employee or client.
+ *
+ * <p>An intent with this action can be sent only on an unprovisioned device.
+ * It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
+ *
+ * <p>The intent contains the following extras:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_MAIN_COLOR}, optional</li>
+ * </ul>
+ *
+ * <p>When device owner provisioning has completed, an intent of the type
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcast to the
+ * device owner.
+ *
+ * <p>From version {@link android.os.Build.VERSION_CODES#O}, when device owner provisioning has
+ * completed, along with the above broadcast, activity intent
+ * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the device owner.
+ *
+ * <p>If provisioning fails, the device is factory reset.
+ *
+ * <p>A result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part
+ * of the provisioning flow was successful, although this doesn't guarantee the full flow will
+ * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies
+ * that the user backed-out of provisioning, or some precondition for provisioning wasn't met.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE
+ = "android.app.action.PROVISION_MANAGED_SHAREABLE_DEVICE";
+
+ /**
+ * Activity action: Finalizes management provisioning, should be used after user-setup
+ * has been completed and {@link #getUserProvisioningState()} returns one of:
+ * <ul>
+ * <li>{@link #STATE_USER_SETUP_INCOMPLETE}</li>
+ * <li>{@link #STATE_USER_SETUP_COMPLETE}</li>
+ * <li>{@link #STATE_USER_PROFILE_COMPLETE}</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_FINALIZATION
+ = "android.app.action.PROVISION_FINALIZATION";
+
+ /**
+ * Action: Bugreport sharing with device owner has been accepted by the user.
+ *
+ * @hide
+ */
+ public static final String ACTION_BUGREPORT_SHARING_ACCEPTED =
+ "com.android.server.action.REMOTE_BUGREPORT_SHARING_ACCEPTED";
+
+ /**
+ * Action: Bugreport sharing with device owner has been declined by the user.
+ *
+ * @hide
+ */
+ public static final String ACTION_BUGREPORT_SHARING_DECLINED =
+ "com.android.server.action.REMOTE_BUGREPORT_SHARING_DECLINED";
+
+ /**
+ * Action: Bugreport has been collected and is dispatched to {@code DevicePolicyManagerService}.
+ *
+ * @hide
+ */
+ public static final String ACTION_REMOTE_BUGREPORT_DISPATCH =
+ "android.intent.action.REMOTE_BUGREPORT_DISPATCH";
+
+ /**
+ * Extra for shared bugreport's SHA-256 hash.
+ *
+ * @hide
+ */
+ public static final String EXTRA_REMOTE_BUGREPORT_HASH =
+ "android.intent.extra.REMOTE_BUGREPORT_HASH";
+
+ /**
+ * Extra for remote bugreport notification shown type.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BUGREPORT_NOTIFICATION_TYPE =
+ "android.app.extra.bugreport_notification_type";
+
+ /**
+ * Notification type for a started remote bugreport flow.
+ *
+ * @hide
+ */
+ public static final int NOTIFICATION_BUGREPORT_STARTED = 1;
+
+ /**
+ * Notification type for a bugreport that has already been accepted to be shared, but is still
+ * being taken.
+ *
+ * @hide
+ */
+ public static final int NOTIFICATION_BUGREPORT_ACCEPTED_NOT_FINISHED = 2;
+
+ /**
+ * Notification type for a bugreport that has been taken and can be shared or declined.
+ *
+ * @hide
+ */
+ public static final int NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED = 3;
+
+ /**
+ * Default and maximum timeout in milliseconds after which unlocking with weak auth times out,
+ * i.e. the user has to use a strong authentication method like password, PIN or pattern.
+ *
+ * @hide
+ */
+ public static final long DEFAULT_STRONG_AUTH_TIMEOUT_MS = 72 * 60 * 60 * 1000; // 72h
+
+ /**
+ * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that
+ * allows a mobile device management application or NFC programmer application which starts
+ * managed provisioning to pass data to the management application instance after provisioning.
+ * <p>
+ * If used with {@link #ACTION_PROVISION_MANAGED_PROFILE} it can be used by the application that
+ * sends the intent to pass data to itself on the newly created profile.
+ * If used with {@link #ACTION_PROVISION_MANAGED_DEVICE} it allows passing data to the same
+ * instance of the app on the primary user.
+ * Starting from {@link android.os.Build.VERSION_CODES#M}, if used with
+ * {@link #MIME_TYPE_PROVISIONING_NFC} as part of NFC managed device provisioning, the NFC
+ * message should contain a stringified {@link java.util.Properties} instance, whose string
+ * properties will be converted into a {@link android.os.PersistableBundle} and passed to the
+ * management application after provisioning.
+ *
+ * <p>
+ * In both cases the application receives the data in
+ * {@link DeviceAdminReceiver#onProfileProvisioningComplete} via an intent with the action
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE}. The bundle is not changed
+ * during the managed provisioning.
+ */
+ public static final String EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE =
+ "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE";
+
+ /**
+ * A String extra holding the package name of the mobile device management application that
+ * will be set as the profile owner or device owner.
+ *
+ * <p>If an application starts provisioning directly via an intent with action
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE} this package has to match the package name of the
+ * application that started provisioning. The package will be set as profile owner in that case.
+ *
+ * <p>This package is set as device owner when device owner provisioning is started by an NFC
+ * message containing an NFC record with MIME type {@link #MIME_TYPE_PROVISIONING_NFC}.
+ *
+ * <p> When this extra is set, the application must have exactly one device admin receiver.
+ * This receiver will be set as the profile or device owner and active admin.
+ *
+ * @see DeviceAdminReceiver
+ * @deprecated Use {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}. This extra is still
+ * supported, but only if there is only one device admin receiver in the package that requires
+ * the permission {@link android.Manifest.permission#BIND_DEVICE_ADMIN}.
+ */
+ @Deprecated
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME";
+
+ /**
+ * A ComponentName extra indicating the device admin receiver of the mobile device management
+ * application that will be set as the profile owner or device owner and active admin.
+ *
+ * <p>If an application starts provisioning directly via an intent with action
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE} or
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE} the package name of this
+ * component has to match the package name of the application that started provisioning.
+ *
+ * <p>This component is set as device owner and active admin when device owner provisioning is
+ * started by an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE} or by an NFC
+ * message containing an NFC record with MIME type
+ * {@link #MIME_TYPE_PROVISIONING_NFC}. For the NFC record, the component name must be
+ * flattened to a string, via {@link ComponentName#flattenToShortString()}.
+ *
+ * @see DeviceAdminReceiver
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME";
+
+ /**
+ * An {@link android.accounts.Account} extra holding the account to migrate during managed
+ * profile provisioning. If the account supplied is present in the primary user, it will be
+ * copied, along with its credentials to the managed profile and removed from the primary user.
+ *
+ * Use with {@link #ACTION_PROVISION_MANAGED_PROFILE}.
+ */
+
+ public static final String EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE
+ = "android.app.extra.PROVISIONING_ACCOUNT_TO_MIGRATE";
+
+ /**
+ * Boolean extra to indicate that the migrated account should be kept. This is used in
+ * conjunction with {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}. If it's set to {@code true},
+ * the account will not be removed from the primary user after it is migrated to the newly
+ * created user or profile.
+ *
+ * <p> Defaults to {@code false}
+ *
+ * <p> Use with {@link #ACTION_PROVISION_MANAGED_PROFILE} and
+ * {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}
+ */
+ public static final String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION
+ = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
+
+ /**
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}, never used while provisioning the
+ * device.
+ */
+ @Deprecated
+ public static final String EXTRA_PROVISIONING_EMAIL_ADDRESS
+ = "android.app.extra.PROVISIONING_EMAIL_ADDRESS";
+
+ /**
+ * A integer extra indicating the predominant color to show during the provisioning.
+ * Refer to {@link android.graphics.Color} for how the color is represented.
+ *
+ * <p>Use with {@link #ACTION_PROVISION_MANAGED_PROFILE} or
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE}.
+ */
+ public static final String EXTRA_PROVISIONING_MAIN_COLOR =
+ "android.app.extra.PROVISIONING_MAIN_COLOR";
+
+ /**
+ * A Boolean extra that can be used by the mobile device management application to skip the
+ * disabling of system apps during provisioning when set to {@code true}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} or an intent with action
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE} that starts device owner provisioning.
+ */
+ public static final String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED =
+ "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED";
+
+ /**
+ * A String extra holding the time zone {@link android.app.AlarmManager} that the device
+ * will be set to.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_TIME_ZONE
+ = "android.app.extra.PROVISIONING_TIME_ZONE";
+
+ /**
+ * A Long extra holding the wall clock time (in milliseconds) to be set on the device's
+ * {@link android.app.AlarmManager}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_LOCAL_TIME
+ = "android.app.extra.PROVISIONING_LOCAL_TIME";
+
+ /**
+ * A String extra holding the {@link java.util.Locale} that the device will be set to.
+ * Format: xx_yy, where xx is the language code, and yy the country code.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_LOCALE
+ = "android.app.extra.PROVISIONING_LOCALE";
+
+ /**
+ * A String extra holding the ssid of the wifi network that should be used during nfc device
+ * owner provisioning for downloading the mobile device management application.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_SSID
+ = "android.app.extra.PROVISIONING_WIFI_SSID";
+
+ /**
+ * A boolean extra indicating whether the wifi network in {@link #EXTRA_PROVISIONING_WIFI_SSID}
+ * is hidden or not.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_HIDDEN
+ = "android.app.extra.PROVISIONING_WIFI_HIDDEN";
+
+ /**
+ * A String extra indicating the security type of the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID} and could be one of {@code NONE}, {@code WPA} or
+ * {@code WEP}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_SECURITY_TYPE
+ = "android.app.extra.PROVISIONING_WIFI_SECURITY_TYPE";
+
+ /**
+ * A String extra holding the password of the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_PASSWORD
+ = "android.app.extra.PROVISIONING_WIFI_PASSWORD";
+
+ /**
+ * A String extra holding the proxy host for the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_PROXY_HOST
+ = "android.app.extra.PROVISIONING_WIFI_PROXY_HOST";
+
+ /**
+ * An int extra holding the proxy port for the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_PROXY_PORT
+ = "android.app.extra.PROVISIONING_WIFI_PROXY_PORT";
+
+ /**
+ * A String extra holding the proxy bypass for the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_PROXY_BYPASS
+ = "android.app.extra.PROVISIONING_WIFI_PROXY_BYPASS";
+
+ /**
+ * A String extra holding the proxy auto-config (PAC) URL for the wifi network in
+ * {@link #EXTRA_PROVISIONING_WIFI_SSID}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_WIFI_PAC_URL
+ = "android.app.extra.PROVISIONING_WIFI_PAC_URL";
+
+ /**
+ * A String extra holding a url that specifies the download location of the device admin
+ * package. When not provided it is assumed that the device admin package is already installed.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION";
+
+ /**
+ * A String extra holding the localized name of the organization under management.
+ *
+ * The name is displayed only during provisioning.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE}
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_PROVISIONING_ORGANIZATION_NAME =
+ "android.app.extra.PROVISIONING_ORGANIZATION_NAME";
+
+ /**
+ * A String extra holding a url to the website of the device provider so the user can open it
+ * during provisioning. If the url is not HTTPS, an error will be shown.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE}
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_PROVISIONING_SUPPORT_URL =
+ "android.app.extra.PROVISIONING_SUPPORT_URL";
+
+ /**
+ * A String extra holding the localized name of the device admin package. It should be the same
+ * as the app label of the package.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE}
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_LABEL =
+ "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_LABEL";
+
+ /**
+ * A {@link Uri} extra pointing to the app icon of device admin package. This image will be
+ * shown during the provisioning.
+ * <h5>The following URI schemes are accepted:</h5>
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * </ul>
+ *
+ * <p> It is the responsibility of the caller to provide an image with a reasonable
+ * pixel density for the device.
+ *
+ * <p> If a content: URI is passed, the intent should have the flag
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the
+ * {@link android.content.ClipData} of the intent too.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE}
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_ICON_URI =
+ "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_ICON_URI";
+
+ /**
+ * An int extra holding a minimum required version code for the device admin package. If the
+ * device admin is already installed on the device, it will only be re-downloaded from
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION} if the version of the
+ * installed package is less than this version code.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_MINIMUM_VERSION_CODE";
+
+ /**
+ * A String extra holding a http cookie header which should be used in the http request to the
+ * url specified in {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER";
+
+ /**
+ * A String extra holding the URL-safe base64 encoded SHA-256 or SHA-1 hash (see notes below) of
+ * the file at download location specified in
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}.
+ *
+ * <p>Either this extra or {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM} must be
+ * present. The provided checksum must match the checksum of the file at the download
+ * location. If the checksum doesn't match an error will be shown to the user and the user will
+ * be asked to factory reset the device.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ *
+ * <p><strong>Note:</strong> for devices running {@link android.os.Build.VERSION_CODES#LOLLIPOP}
+ * and {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} only SHA-1 hash is supported.
+ * Starting from {@link android.os.Build.VERSION_CODES#M}, this parameter accepts SHA-256 in
+ * addition to SHA-1. Support for SHA-1 is likely to be removed in future OS releases.
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM";
+
+ /**
+ * A String extra holding the URL-safe base64 encoded SHA-256 checksum of any signature of the
+ * android package archive at the download location specified in {@link
+ * #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}.
+ *
+ * <p>The signatures of an android package archive can be obtained using
+ * {@link android.content.pm.PackageManager#getPackageArchiveInfo} with flag
+ * {@link android.content.pm.PackageManager#GET_SIGNATURES}.
+ *
+ * <p>Either this extra or {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM} must be
+ * present. The provided checksum must match the checksum of any signature of the file at
+ * the download location. If the checksum does not match an error will be shown to the user and
+ * the user will be asked to factory reset the device.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} that starts device owner
+ * provisioning via an NFC bump.
+ */
+ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM
+ = "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM";
+
+ /**
+ * Broadcast Action: This broadcast is sent to indicate that provisioning of a managed profile
+ * has completed successfully.
+ *
+ * <p>The broadcast is limited to the primary profile, to the app specified in the provisioning
+ * intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE}.
+ *
+ * <p>This intent will contain the following extras
+ * <ul>
+ * <li>{@link Intent#EXTRA_USER}, corresponds to the {@link UserHandle} of the managed
+ * profile.</li>
+ * <li>{@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}, corresponds to the account requested to
+ * be migrated at provisioning time, if any.</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_MANAGED_PROFILE_PROVISIONED
+ = "android.app.action.MANAGED_PROFILE_PROVISIONED";
+
+ /**
+ * Activity action: This activity action is sent to indicate that provisioning of a managed
+ * profile or managed device has completed successfully. It'll be sent at the same time as
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast but this will be
+ * delivered faster as it's an activity intent.
+ *
+ * <p>The intent is only sent to the new device or profile owner.
+ *
+ * @see #ACTION_PROVISION_MANAGED_PROFILE
+ * @see #ACTION_PROVISION_MANAGED_DEVICE
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISIONING_SUCCESSFUL =
+ "android.app.action.PROVISIONING_SUCCESSFUL";
+
+ /**
+ * A boolean extra indicating whether device encryption can be skipped as part of device owner
+ * or managed profile provisioning.
+ *
+ * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} or an intent with action
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE} that starts device owner provisioning.
+ *
+ * <p>From {@link android.os.Build.VERSION_CODES#N} onwards, this is also supported for an
+ * intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE}.
+ */
+ public static final String EXTRA_PROVISIONING_SKIP_ENCRYPTION =
+ "android.app.extra.PROVISIONING_SKIP_ENCRYPTION";
+
+ /**
+ * A {@link Uri} extra pointing to a logo image. This image will be shown during the
+ * provisioning. If this extra is not passed, a default image will be shown.
+ * <h5>The following URI schemes are accepted:</h5>
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * </ul>
+ *
+ * <p> It is the responsibility of the caller to provide an image with a reasonable
+ * pixel density for the device.
+ *
+ * <p> If a content: URI is passed, the intent should have the flag
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the
+ * {@link android.content.ClipData} of the intent too.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE} or
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE}
+ */
+ public static final String EXTRA_PROVISIONING_LOGO_URI =
+ "android.app.extra.PROVISIONING_LOGO_URI";
+
+ /**
+ * A {@link Bundle}[] extra consisting of list of disclaimer headers and disclaimer contents.
+ * Each {@link Bundle} must have both {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER}
+ * as disclaimer header, and {@link #EXTRA_PROVISIONING_DISCLAIMER_CONTENT} as disclaimer
+ * content.
+ *
+ * <p> The extra typically contains one disclaimer from the company of mobile device
+ * management application (MDM), and one disclaimer from the organization.
+ *
+ * <p> Call {@link Bundle#putParcelableArray(String, Parcelable[])} to put the {@link Bundle}[]
+ *
+ * <p> Maximum 3 key-value pairs can be specified. The rest will be ignored.
+ *
+ * <p> Use in an intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE} or
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE}
+ */
+ public static final String EXTRA_PROVISIONING_DISCLAIMERS =
+ "android.app.extra.PROVISIONING_DISCLAIMERS";
+
+ /**
+ * A String extra of localized disclaimer header.
+ *
+ * <p> The extra is typically the company name of mobile device management application (MDM)
+ * or the organization name.
+ *
+ * <p> Use in Bundle {@link #EXTRA_PROVISIONING_DISCLAIMERS}
+ *
+ * <p> System app, i.e. application with {@link ApplicationInfo#FLAG_SYSTEM}, can also insert a
+ * disclaimer by declaring an application-level meta-data in {@code AndroidManifest.xml}.
+ * Must use it with {@link #EXTRA_PROVISIONING_DISCLAIMER_CONTENT}. Here is the example:
+ *
+ * <pre>
+ * &lt;meta-data
+ * android:name="android.app.extra.PROVISIONING_DISCLAIMER_HEADER"
+ * android:resource="@string/disclaimer_header"
+ * /&gt;</pre>
+ */
+ public static final String EXTRA_PROVISIONING_DISCLAIMER_HEADER =
+ "android.app.extra.PROVISIONING_DISCLAIMER_HEADER";
+
+ /**
+ * A {@link Uri} extra pointing to disclaimer content.
+ *
+ * <h5>The following URI schemes are accepted:</h5>
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * </ul>
+ *
+ * <p> Styled text is supported in the disclaimer content. The content is parsed by
+ * {@link android.text.Html#fromHtml(String)} and displayed in a
+ * {@link android.widget.TextView}.
+ *
+ * <p> If a <code>content:</code> URI is passed, URI is passed, the intent should have the flag
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the
+ * {@link android.content.ClipData} of the intent too.
+ *
+ * <p> Use in Bundle {@link #EXTRA_PROVISIONING_DISCLAIMERS}
+ *
+ * <p> System app, i.e. application with {@link ApplicationInfo#FLAG_SYSTEM}, can also insert a
+ * disclaimer by declaring an application-level meta-data in {@code AndroidManifest.xml}.
+ * Must use it with {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER}. Here is the example:
+ *
+ * <pre>
+ * &lt;meta-data
+ * android:name="android.app.extra.PROVISIONING_DISCLAIMER_CONTENT"
+ * android:resource="@string/disclaimer_content"
+ * /&gt;</pre>
+ */
+ public static final String EXTRA_PROVISIONING_DISCLAIMER_CONTENT =
+ "android.app.extra.PROVISIONING_DISCLAIMER_CONTENT";
+
+ /**
+ * A boolean extra indicating if user setup should be skipped, for when provisioning is started
+ * during setup-wizard.
+ *
+ * <p>If unspecified, defaults to {@code true} to match the behavior in
+ * {@link android.os.Build.VERSION_CODES#M} and earlier.
+ *
+ * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_DEVICE} or
+ * {@link #ACTION_PROVISION_MANAGED_USER}.
+ *
+ * @hide
+ */
+ public static final String EXTRA_PROVISIONING_SKIP_USER_SETUP =
+ "android.app.extra.PROVISIONING_SKIP_USER_SETUP";
+
+ /**
+ * A boolean extra indicating if the user consent steps from the provisioning flow should be
+ * skipped. If unspecified, defaults to {@code false}.
+ *
+ * It can only be used by an existing device owner trying to create a managed profile via
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE}. Otherwise it is ignored.
+ */
+ public static final String EXTRA_PROVISIONING_SKIP_USER_CONSENT =
+ "android.app.extra.PROVISIONING_SKIP_USER_CONSENT";
+
+ /**
+ * This MIME type is used for starting the device owner provisioning.
+ *
+ * <p>During device owner provisioning a device admin app is set as the owner of the device.
+ * A device owner has full control over the device. The device owner can not be modified by the
+ * user and the only way of resetting the device is if the device owner app calls a factory
+ * reset.
+ *
+ * <p> A typical use case would be a device that is owned by a company, but used by either an
+ * employee or client.
+ *
+ * <p> The NFC message must be sent to an unprovisioned device.
+ *
+ * <p>The NFC record must contain a serialized {@link java.util.Properties} object which
+ * contains the following properties:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_COOKIE_HEADER}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOCAL_TIME} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_TIME_ZONE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LOCALE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_SSID}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_HIDDEN} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_SECURITY_TYPE}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PASSWORD}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_HOST}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_PORT} (convert to String), optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PROXY_BYPASS}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_WIFI_PAC_URL}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional, supported from
+ * {@link android.os.Build.VERSION_CODES#M} </li></ul>
+ *
+ * <p>
+ * As of {@link android.os.Build.VERSION_CODES#M}, the properties should contain
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead of
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}, (although specifying only
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is still supported).
+ */
+ public static final String MIME_TYPE_PROVISIONING_NFC
+ = "application/com.android.managedprovisioning";
+
+ /**
+ * Activity action: ask the user to add a new device administrator to the system.
+ * The desired policy is the ComponentName of the policy in the
+ * {@link #EXTRA_DEVICE_ADMIN} extra field. This will invoke a UI to
+ * bring the user through adding the device administrator to the system (or
+ * allowing them to reject it).
+ *
+ * <p>You can optionally include the {@link #EXTRA_ADD_EXPLANATION}
+ * field to provide the user with additional explanation (in addition
+ * to your component's description) about what is being added.
+ *
+ * <p>If your administrator is already active, this will ordinarily return immediately (without
+ * user intervention). However, if your administrator has been updated and is requesting
+ * additional uses-policy flags, the user will be presented with the new list. New policies
+ * will not be available to the updated administrator until the user has accepted the new list.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ADD_DEVICE_ADMIN
+ = "android.app.action.ADD_DEVICE_ADMIN";
+
+ /**
+ * @hide
+ * Activity action: ask the user to add a new device administrator as the profile owner
+ * for this user. Only system apps can launch this intent.
+ *
+ * <p>The ComponentName of the profile owner admin is passed in the {@link #EXTRA_DEVICE_ADMIN}
+ * extra field. This will invoke a UI to bring the user through adding the profile owner admin
+ * to remotely control restrictions on the user.
+ *
+ * <p>The intent must be invoked via {@link Activity#startActivityForResult} to receive the
+ * result of whether or not the user approved the action. If approved, the result will
+ * be {@link Activity#RESULT_OK} and the component will be set as an active admin as well
+ * as a profile owner.
+ *
+ * <p>You can optionally include the {@link #EXTRA_ADD_EXPLANATION}
+ * field to provide the user with additional explanation (in addition
+ * to your component's description) about what is being added.
+ *
+ * <p>If there is already a profile owner active or the caller is not a system app, the
+ * operation will return a failure result.
+ */
+ @SystemApi
+ public static final String ACTION_SET_PROFILE_OWNER
+ = "android.app.action.SET_PROFILE_OWNER";
+
+ /**
+ * @hide
+ * Name of the profile owner admin that controls the user.
+ */
+ @SystemApi
+ public static final String EXTRA_PROFILE_OWNER_NAME
+ = "android.app.extra.PROFILE_OWNER_NAME";
+
+ /**
+ * Broadcast action: send when any policy admin changes a policy.
+ * This is generally used to find out when a new policy is in effect.
+ *
+ * @hide
+ */
+ public static final String ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
+ = "android.app.action.DEVICE_POLICY_MANAGER_STATE_CHANGED";
+
+ /**
+ * Broadcast action: sent when the device owner is set, changed or cleared.
+ *
+ * This broadcast is sent only to the primary user.
+ * @see #ACTION_PROVISION_MANAGED_DEVICE
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DEVICE_OWNER_CHANGED
+ = "android.app.action.DEVICE_OWNER_CHANGED";
+
+ /**
+ * The ComponentName of the administrator component.
+ *
+ * @see #ACTION_ADD_DEVICE_ADMIN
+ */
+ public static final String EXTRA_DEVICE_ADMIN = "android.app.extra.DEVICE_ADMIN";
+
+ /**
+ * An optional CharSequence providing additional explanation for why the
+ * admin is being added.
+ *
+ * @see #ACTION_ADD_DEVICE_ADMIN
+ */
+ public static final String EXTRA_ADD_EXPLANATION = "android.app.extra.ADD_EXPLANATION";
+
+ /**
+ * Constant to indicate the feature of disabling the camera. Used as argument to
+ * {@link #createAdminSupportIntent(String)}.
+ * @see #setCameraDisabled(ComponentName, boolean)
+ */
+ public static final String POLICY_DISABLE_CAMERA = "policy_disable_camera";
+
+ /**
+ * Constant to indicate the feature of disabling screen captures. Used as argument to
+ * {@link #createAdminSupportIntent(String)}.
+ * @see #setScreenCaptureDisabled(ComponentName, boolean)
+ */
+ public static final String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
+
+ /**
+ * A String indicating a specific restricted feature. Can be a user restriction from the
+ * {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values
+ * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * @see #createAdminSupportIntent(String)
+ * @hide
+ */
+ @TestApi
+ public static final String EXTRA_RESTRICTION = "android.app.extra.RESTRICTION";
+
+ /**
+ * Activity action: have the user enter a new password. This activity should
+ * be launched after using {@link #setPasswordQuality(ComponentName, int)},
+ * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the user
+ * enter a new password that meets the current requirements. You can use
+ * {@link #isActivePasswordSufficient()} to determine whether you need to
+ * have the user select a new password in order to meet the current
+ * constraints. Upon being resumed from this activity, you can check the new
+ * password characteristics to see if they are sufficient.
+ *
+ * If the intent is launched from within a managed profile with a profile
+ * owner built against {@link android.os.Build.VERSION_CODES#M} or before,
+ * this will trigger entering a new password for the parent of the profile.
+ * For all other cases it will trigger entering a new password for the user
+ * or profile it is launched from.
+ *
+ * @see #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SET_NEW_PASSWORD
+ = "android.app.action.SET_NEW_PASSWORD";
+
+ /**
+ * Activity action: have the user enter a new password for the parent profile.
+ * If the intent is launched from within a managed profile, this will trigger
+ * entering a new password for the parent of the profile. In all other cases
+ * the behaviour is identical to {@link #ACTION_SET_NEW_PASSWORD}.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SET_NEW_PARENT_PROFILE_PASSWORD
+ = "android.app.action.SET_NEW_PARENT_PROFILE_PASSWORD";
+
+ /**
+ * Broadcast action: Tell the status bar to open the device monitoring dialog, e.g. when
+ * Network logging was enabled and the user tapped the notification.
+ * <p class="note">This is a protected intent that can only be sent by the system.</p>
+ * @hide
+ */
+ public static final String ACTION_SHOW_DEVICE_MONITORING_DIALOG
+ = "android.app.action.SHOW_DEVICE_MONITORING_DIALOG";
+
+ /**
+ * Broadcast Action: Sent after application delegation scopes are changed. The new delegation
+ * scopes will be sent in an {@code ArrayList<String>} extra identified by the
+ * {@link #EXTRA_DELEGATION_SCOPES} key.
+ *
+ * <p class=”note”> Note: This is a protected intent that can only be sent by the system.</p>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED =
+ "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
+
+ /**
+ * An {@code ArrayList<String>} corresponding to the delegation scopes given to an app in the
+ * {@link #ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED} broadcast.
+ */
+ public static final String EXTRA_DELEGATION_SCOPES = "android.app.extra.DELEGATION_SCOPES";
+
+ /**
+ * Flag used by {@link #addCrossProfileIntentFilter} to allow activities in
+ * the parent profile to access intents sent from the managed profile.
+ * That is, when an app in the managed profile calls
+ * {@link Activity#startActivity(Intent)}, the intent can be resolved by a
+ * matching activity in the parent profile.
+ */
+ public static final int FLAG_PARENT_CAN_ACCESS_MANAGED = 0x0001;
+
+ /**
+ * Flag used by {@link #addCrossProfileIntentFilter} to allow activities in
+ * the managed profile to access intents sent from the parent profile.
+ * That is, when an app in the parent profile calls
+ * {@link Activity#startActivity(Intent)}, the intent can be resolved by a
+ * matching activity in the managed profile.
+ */
+ public static final int FLAG_MANAGED_CAN_ACCESS_PARENT = 0x0002;
+
+ /**
+ * Broadcast action: notify that a new local system update policy has been set by the device
+ * owner. The new policy can be retrieved by {@link #getSystemUpdatePolicy()}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SYSTEM_UPDATE_POLICY_CHANGED
+ = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
+
+ /**
+ * Permission policy to prompt user for new permission requests for runtime permissions.
+ * Already granted or denied permissions are not affected by this.
+ */
+ public static final int PERMISSION_POLICY_PROMPT = 0;
+
+ /**
+ * Permission policy to always grant new permission requests for runtime permissions.
+ * Already granted or denied permissions are not affected by this.
+ */
+ public static final int PERMISSION_POLICY_AUTO_GRANT = 1;
+
+ /**
+ * Permission policy to always deny new permission requests for runtime permissions.
+ * Already granted or denied permissions are not affected by this.
+ */
+ public static final int PERMISSION_POLICY_AUTO_DENY = 2;
+
+ /**
+ * Runtime permission state: The user can manage the permission
+ * through the UI.
+ */
+ public static final int PERMISSION_GRANT_STATE_DEFAULT = 0;
+
+ /**
+ * Runtime permission state: The permission is granted to the app
+ * and the user cannot manage the permission through the UI.
+ */
+ public static final int PERMISSION_GRANT_STATE_GRANTED = 1;
+
+ /**
+ * Runtime permission state: The permission is denied to the app
+ * and the user cannot manage the permission through the UI.
+ */
+ public static final int PERMISSION_GRANT_STATE_DENIED = 2;
+
+ /**
+ * Delegation of certificate installation and management. This scope grants access to the
+ * {@link #getInstalledCaCerts}, {@link #hasCaCertInstalled}, {@link #installCaCert},
+ * {@link #uninstallCaCert}, {@link #uninstallAllUserCaCerts} and {@link #installKeyPair} APIs.
+ */
+ public static final String DELEGATION_CERT_INSTALL = "delegation-cert-install";
+
+ /**
+ * Delegation of application restrictions management. This scope grants access to the
+ * {@link #setApplicationRestrictions} and {@link #getApplicationRestrictions} APIs.
+ */
+ public static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions";
+
+ /**
+ * Delegation of application uninstall block. This scope grants access to the
+ * {@link #setUninstallBlocked} API.
+ */
+ public static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall";
+
+ /**
+ * Delegation of permission policy and permission grant state. This scope grants access to the
+ * {@link #setPermissionPolicy}, {@link #getPermissionGrantState},
+ * and {@link #setPermissionGrantState} APIs.
+ */
+ public static final String DELEGATION_PERMISSION_GRANT = "delegation-permission-grant";
+
+ /**
+ * Delegation of package access state. This scope grants access to the
+ * {@link #isApplicationHidden}, {@link #setApplicationHidden}, {@link #isPackageSuspended}, and
+ * {@link #setPackagesSuspended} APIs.
+ */
+ public static final String DELEGATION_PACKAGE_ACCESS = "delegation-package-access";
+
+ /**
+ * Delegation for enabling system apps. This scope grants access to the {@link #enableSystemApp}
+ * API.
+ */
+ public static final String DELEGATION_ENABLE_SYSTEM_APP = "delegation-enable-system-app";
+
+ /**
+ * Delegation of management of uninstalled packages. This scope grants access to the
+ * {@code #setKeepUninstalledPackages} and {@code #getKeepUninstalledPackages} APIs.
+ * @hide
+ */
+ public static final String DELEGATION_KEEP_UNINSTALLED_PACKAGES =
+ "delegation-keep-uninstalled-packages";
+
+ /**
+ * No management for current user in-effect. This is the default.
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_USER_UNMANAGED = 0;
+
+ /**
+ * Management partially setup, user setup needs to be completed.
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_USER_SETUP_INCOMPLETE = 1;
+
+ /**
+ * Management partially setup, user setup completed.
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_USER_SETUP_COMPLETE = 2;
+
+ /**
+ * Management setup and active on current user.
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_USER_SETUP_FINALIZED = 3;
+
+ /**
+ * Management partially setup on a managed profile.
+ * @hide
+ */
+ @SystemApi
+ public static final int STATE_USER_PROFILE_COMPLETE = 4;
+
+ /**
+ * @hide
+ */
+ @IntDef({STATE_USER_UNMANAGED, STATE_USER_SETUP_INCOMPLETE, STATE_USER_SETUP_COMPLETE,
+ STATE_USER_SETUP_FINALIZED, STATE_USER_PROFILE_COMPLETE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UserProvisioningState {}
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE}, {@link #ACTION_PROVISION_MANAGED_USER} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} when provisioning is allowed.
+ *
+ * @hide
+ */
+ public static final int CODE_OK = 0;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} when the device already has a device
+ * owner.
+ *
+ * @hide
+ */
+ public static final int CODE_HAS_DEVICE_OWNER = 1;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} when the user has a profile owner and for
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE} when the profile owner is already set.
+ *
+ * @hide
+ */
+ public static final int CODE_USER_HAS_PROFILE_OWNER = 2;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} when the user isn't running.
+ *
+ * @hide
+ */
+ public static final int CODE_USER_NOT_RUNNING = 3;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} if the device has already been setup and
+ * for {@link #ACTION_PROVISION_MANAGED_USER} if the user has already been setup.
+ *
+ * @hide
+ */
+ public static final int CODE_USER_SETUP_COMPLETED = 4;
+
+ /**
+ * Code used to indicate that the device also has a user other than the system user.
+ *
+ * @hide
+ */
+ public static final int CODE_NONSYSTEM_USER_EXISTS = 5;
+
+ /**
+ * Code used to indicate that device has an account that prevents provisioning.
+ *
+ * @hide
+ */
+ public static final int CODE_ACCOUNTS_NOT_EMPTY = 6;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} if the user is not a system user.
+ *
+ * @hide
+ */
+ public static final int CODE_NOT_SYSTEM_USER = 7;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} and {@link #ACTION_PROVISION_MANAGED_USER}
+ * when the device is a watch and is already paired.
+ *
+ * @hide
+ */
+ public static final int CODE_HAS_PAIRED = 8;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} and
+ * {@link #ACTION_PROVISION_MANAGED_USER} on devices which do not support managed users.
+ *
+ * @see {@link PackageManager#FEATURE_MANAGED_USERS}
+ * @hide
+ */
+ public static final int CODE_MANAGED_USERS_NOT_SUPPORTED = 9;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_USER} if the user is a system user.
+ *
+ * @hide
+ */
+ public static final int CODE_SYSTEM_USER = 10;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} when the user cannot have more
+ * managed profiles.
+ *
+ * @hide
+ */
+ public static final int CODE_CANNOT_ADD_MANAGED_PROFILE = 11;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_USER} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} on devices not running with split system
+ * user.
+ *
+ * @hide
+ */
+ public static final int CODE_NOT_SYSTEM_USER_SPLIT = 12;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE}, {@link #ACTION_PROVISION_MANAGED_USER} and
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE} on devices which do no support device
+ * admins.
+ *
+ * @hide
+ */
+ public static final int CODE_DEVICE_ADMIN_NOT_SUPPORTED = 13;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} when the device the user is a
+ * system user on a split system user device.
+ *
+ * @hide
+ */
+ public static final int CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER = 14;
+
+ /**
+ * Result code for {@link #checkProvisioningPreCondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} when adding a managed profile is
+ * disallowed by {@link UserManager#DISALLOW_ADD_MANAGED_PROFILE}.
+ *
+ * @hide
+ */
+ public static final int CODE_ADD_MANAGED_PROFILE_DISALLOWED = 15;
+
+ /**
+ * Result codes for {@link #checkProvisioningPreCondition} indicating all the provisioning pre
+ * conditions.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CODE_OK, CODE_HAS_DEVICE_OWNER, CODE_USER_HAS_PROFILE_OWNER, CODE_USER_NOT_RUNNING,
+ CODE_USER_SETUP_COMPLETED, CODE_NOT_SYSTEM_USER, CODE_HAS_PAIRED,
+ CODE_MANAGED_USERS_NOT_SUPPORTED, CODE_SYSTEM_USER, CODE_CANNOT_ADD_MANAGED_PROFILE,
+ CODE_NOT_SYSTEM_USER_SPLIT, CODE_DEVICE_ADMIN_NOT_SUPPORTED,
+ CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER, CODE_ADD_MANAGED_PROFILE_DISALLOWED})
+ public @interface ProvisioningPreCondition {}
+
+ /**
+ * Service action: Action for a service that device owner and profile owner can optionally
+ * own. If a device owner or a profile owner has such a service, the system tries to keep
+ * a bound connection to it, in order to keep their process always running.
+ * The service must be protected with the {@link android.Manifest.permission#BIND_DEVICE_ADMIN}
+ * permission.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String ACTION_DEVICE_ADMIN_SERVICE
+ = "android.app.action.DEVICE_ADMIN_SERVICE";
+
+ /**
+ * Return true if the given administrator component is currently active (enabled) in the system.
+ *
+ * @param admin The administrator component to check for.
+ * @return {@code true} if {@code admin} is currently enabled in the system, {@code false}
+ * otherwise
+ */
+ public boolean isAdminActive(@NonNull ComponentName admin) {
+ throwIfParentInstance("isAdminActive");
+ return isAdminActiveAsUser(admin, myUserId());
+ }
+
+ /**
+ * @see #isAdminActive(ComponentName)
+ * @hide
+ */
+ public boolean isAdminActiveAsUser(@NonNull ComponentName admin, int userId) {
+ if (mService != null) {
+ try {
+ return mService.isAdminActive(admin, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the given administrator component is currently being removed
+ * for the user.
+ * @hide
+ */
+ public boolean isRemovingAdmin(@NonNull ComponentName admin, int userId) {
+ if (mService != null) {
+ try {
+ return mService.isRemovingAdmin(admin, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return a list of all currently active device administrators' component
+ * names. If there are no administrators {@code null} may be
+ * returned.
+ */
+ public @Nullable List<ComponentName> getActiveAdmins() {
+ throwIfParentInstance("getActiveAdmins");
+ return getActiveAdminsAsUser(myUserId());
+ }
+
+ /**
+ * @see #getActiveAdmins()
+ * @hide
+ */
+ public @Nullable List<ComponentName> getActiveAdminsAsUser(int userId) {
+ if (mService != null) {
+ try {
+ return mService.getActiveAdmins(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Used by package administration code to determine if a package can be stopped
+ * or uninstalled.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ public boolean packageHasActiveAdmins(String packageName) {
+ return packageHasActiveAdmins(packageName, myUserId());
+ }
+
+ /**
+ * Used by package administration code to determine if a package can be stopped
+ * or uninstalled.
+ * @hide
+ */
+ public boolean packageHasActiveAdmins(String packageName, int userId) {
+ if (mService != null) {
+ try {
+ return mService.packageHasActiveAdmins(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove a current administration component. This can only be called
+ * by the application that owns the administration component; if you
+ * try to remove someone else's component, a security exception will be
+ * thrown.
+ *
+ * <p>Note that the operation is not synchronous and the admin might still be active (as
+ * indicated by {@link #getActiveAdmins()}) by the time this method returns.
+ *
+ * @param admin The administration compononent to remove.
+ * @throws SecurityException if the caller is not in the owner application of {@code admin}.
+ */
+ public void removeActiveAdmin(@NonNull ComponentName admin) {
+ throwIfParentInstance("removeActiveAdmin");
+ if (mService != null) {
+ try {
+ mService.removeActiveAdmin(admin, myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns true if an administrator has been granted a particular device policy. This can be
+ * used to check whether the administrator was activated under an earlier set of policies, but
+ * requires additional policies after an upgrade.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Must be an
+ * active administrator, or an exception will be thrown.
+ * @param usesPolicy Which uses-policy to check, as defined in {@link DeviceAdminInfo}.
+ * @throws SecurityException if {@code admin} is not an active administrator.
+ */
+ public boolean hasGrantedPolicy(@NonNull ComponentName admin, int usesPolicy) {
+ throwIfParentInstance("hasGrantedPolicy");
+ if (mService != null) {
+ try {
+ return mService.hasGrantedPolicy(admin, usesPolicy, myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the Profile Challenge is available to use for the given profile user.
+ *
+ * @hide
+ */
+ public boolean isSeparateProfileChallengeAllowed(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.isSeparateProfileChallengeAllowed(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the policy has no requirements
+ * for the password. Note that quality constants are ordered so that higher
+ * values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_UNSPECIFIED = 0;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the policy allows for low-security biometric
+ * recognition technology. This implies technologies that can recognize the identity of
+ * an individual to about a 3 digit PIN (false detection is less than 1 in 1,000).
+ * Note that quality constants are ordered so that higher values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_BIOMETRIC_WEAK = 0x8000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the policy requires some kind
+ * of password or pattern, but doesn't care what it is. Note that quality constants
+ * are ordered so that higher values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_SOMETHING = 0x10000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least numeric characters. Note that quality
+ * constants are ordered so that higher values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_NUMERIC = 0x20000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least numeric characters with no repeating (4444)
+ * or ordered (1234, 4321, 2468) sequences. Note that quality
+ * constants are ordered so that higher values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_NUMERIC_COMPLEX = 0x30000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least alphabetic (or other symbol) characters.
+ * Note that quality constants are ordered so that higher values are more
+ * restrictive.
+ */
+ public static final int PASSWORD_QUALITY_ALPHABETIC = 0x40000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least <em>both></em> numeric <em>and</em>
+ * alphabetic (or other symbol) characters. Note that quality constants are
+ * ordered so that higher values are more restrictive.
+ */
+ public static final int PASSWORD_QUALITY_ALPHANUMERIC = 0x50000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least a letter, a numerical digit and a special
+ * symbol, by default. With this password quality, passwords can be
+ * restricted to contain various sets of characters, like at least an
+ * uppercase letter, etc. These are specified using various methods,
+ * like {@link #setPasswordMinimumLowerCase(ComponentName, int)}. Note
+ * that quality constants are ordered so that higher values are more
+ * restrictive.
+ */
+ public static final int PASSWORD_QUALITY_COMPLEX = 0x60000;
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user is not allowed to
+ * modify password. In case this password quality is set, the password is
+ * managed by a profile owner. The profile owner can set any password,
+ * as if {@link #PASSWORD_QUALITY_UNSPECIFIED} is used. Note
+ * that quality constants are ordered so that higher values are more
+ * restrictive. The value of {@link #PASSWORD_QUALITY_MANAGED} is
+ * the highest.
+ * @hide
+ */
+ public static final int PASSWORD_QUALITY_MANAGED = 0x80000;
+
+ /**
+ * @hide
+ *
+ * adb shell dpm set-{device,profile}-owner will normally not allow installing an owner to
+ * a user with accounts. {@link #ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_ALLOWED}
+ * and {@link #ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_DISALLOWED} are the account features
+ * used by authenticator to exempt their accounts from this:
+ *
+ * <ul>
+ * <li>Non-test-only DO/PO still can't be installed when there are accounts.
+ * <p>In order to make an apk test-only, add android:testOnly="true" to the
+ * &lt;application&gt; tag in the manifest.
+ *
+ * <li>Test-only DO/PO can be installed even when there are accounts, as long as all the
+ * accounts have the {@link #ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_ALLOWED} feature.
+ * Some authenticators claim to have any features, so to detect it, we also check
+ * {@link #ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_DISALLOWED} and disallow installing
+ * if any of the accounts have it.
+ * </ul>
+ */
+ @SystemApi
+ @TestApi
+ public static final String ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_ALLOWED =
+ "android.account.DEVICE_OR_PROFILE_OWNER_ALLOWED";
+
+ /** @hide See {@link #ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_ALLOWED} */
+ @SystemApi
+ @TestApi
+ public static final String ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_DISALLOWED =
+ "android.account.DEVICE_OR_PROFILE_OWNER_DISALLOWED";
+
+ /**
+ * Called by an application that is administering the device to set the password restrictions it
+ * is imposing. After setting this, the user will not be able to enter a new password that is
+ * not at least as restrictive as what has been set. Note that the current password will remain
+ * until the user has set a new one, so the change does not take place immediately. To prompt
+ * the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
+ * {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after calling this method.
+ * <p>
+ * Quality constants are ordered so that higher values are more restrictive; thus the highest
+ * requested quality constant (between the policy set here, the user's preference, and any other
+ * considerations) is the one that is in effect.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param quality The new desired quality. One of {@link #PASSWORD_QUALITY_UNSPECIFIED},
+ * {@link #PASSWORD_QUALITY_SOMETHING}, {@link #PASSWORD_QUALITY_NUMERIC},
+ * {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX}, {@link #PASSWORD_QUALITY_ALPHABETIC},
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC} or {@link #PASSWORD_QUALITY_COMPLEX}.
+ * @throws SecurityException if {@code admin} is not an active administrator or if {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordQuality(@NonNull ComponentName admin, int quality) {
+ if (mService != null) {
+ try {
+ mService.setPasswordQuality(admin, quality, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current minimum password quality for a particular admin or all admins that set
+ * restrictions on this user and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * all admins.
+ */
+ public int getPasswordQuality(@Nullable ComponentName admin) {
+ return getPasswordQuality(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordQuality(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordQuality(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return PASSWORD_QUALITY_UNSPECIFIED;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum allowed password
+ * length. After setting this, the user will not be able to enter a new password that is not at
+ * least as restrictive as what has been set. Note that the current password will remain until
+ * the user has set a new one, so the change does not take place immediately. To prompt the user
+ * for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
+ * {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
+ * only imposed if the administrator has also requested either {@link #PASSWORD_QUALITY_NUMERIC}
+ * , {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX}, {@link #PASSWORD_QUALITY_ALPHABETIC},
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX} with
+ * {@link #setPasswordQuality}.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum password length. A value of 0 means there is no
+ * restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumLength(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumLength(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current minimum password length for a particular admin or all admins that set
+ * restrictions on this user and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * user and its profiles or a particular one.
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * all admins.
+ */
+ public int getPasswordMinimumLength(@Nullable ComponentName admin) {
+ return getPasswordMinimumLength(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumLength(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumLength(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of upper
+ * case letters required in the password. After setting this, the user will not be able to enter
+ * a new password that is not at least as restrictive as what has been set. Note that the
+ * current password will remain until the user has set a new one, so the change does not take
+ * place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
+ * setting this value. This constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of upper case letters required in the password.
+ * A value of 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumUpperCase(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumUpperCase(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of upper case letters required in the password
+ * for a particular admin or all admins that set restrictions on this user and
+ * its participating profiles. Restrictions on profiles that have a separate challenge
+ * are not taken into account.
+ * This is the same value as set by
+ * {@link #setPasswordMinimumUpperCase(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of upper case letters required in the
+ * password.
+ */
+ public int getPasswordMinimumUpperCase(@Nullable ComponentName admin) {
+ return getPasswordMinimumUpperCase(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumUpperCase(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumUpperCase(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of lower
+ * case letters required in the password. After setting this, the user will not be able to enter
+ * a new password that is not at least as restrictive as what has been set. Note that the
+ * current password will remain until the user has set a new one, so the change does not take
+ * place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
+ * setting this value. This constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of lower case letters required in the password.
+ * A value of 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumLowerCase(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumLowerCase(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of lower case letters required in the password
+ * for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ * This is the same value as set by
+ * {@link #setPasswordMinimumLowerCase(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of lower case letters required in the
+ * password.
+ */
+ public int getPasswordMinimumLowerCase(@Nullable ComponentName admin) {
+ return getPasswordMinimumLowerCase(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumLowerCase(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumLowerCase(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of
+ * letters required in the password. After setting this, the user will not be able to enter a
+ * new password that is not at least as restrictive as what has been set. Note that the current
+ * password will remain until the user has set a new one, so the change does not take place
+ * immediately. To prompt the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
+ * {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
+ * only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
+ * {@link #setPasswordQuality}. The default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of letters required in the password. A value of
+ * 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumLetters(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumLetters(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of letters required in the password
+ * for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ * This is the same value as set by
+ * {@link #setPasswordMinimumLetters(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of letters required in the password.
+ */
+ public int getPasswordMinimumLetters(@Nullable ComponentName admin) {
+ return getPasswordMinimumLetters(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumLetters(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumLetters(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of
+ * numerical digits required in the password. After setting this, the user will not be able to
+ * enter a new password that is not at least as restrictive as what has been set. Note that the
+ * current password will remain until the user has set a new one, so the change does not take
+ * place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
+ * setting this value. This constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of numerical digits required in the password. A
+ * value of 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumNumeric(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumNumeric(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of numerical digits required in the password
+ * for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ * This is the same value as set by
+ * {@link #setPasswordMinimumNumeric(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of numerical digits required in the password.
+ */
+ public int getPasswordMinimumNumeric(@Nullable ComponentName admin) {
+ return getPasswordMinimumNumeric(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumNumeric(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumNumeric(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of
+ * symbols required in the password. After setting this, the user will not be able to enter a
+ * new password that is not at least as restrictive as what has been set. Note that the current
+ * password will remain until the user has set a new one, so the change does not take place
+ * immediately. To prompt the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
+ * {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
+ * only imposed if the administrator has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
+ * {@link #setPasswordQuality}. The default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of symbols required in the password. A value of
+ * 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumSymbols(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumSymbols(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of symbols required in the password
+ * for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account. This is the same value as
+ * set by {@link #setPasswordMinimumSymbols(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of symbols required in the password.
+ */
+ public int getPasswordMinimumSymbols(@Nullable ComponentName admin) {
+ return getPasswordMinimumSymbols(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumSymbols(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumSymbols(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the minimum number of
+ * non-letter characters (numerical digits or symbols) required in the password. After setting
+ * this, the user will not be able to enter a new password that is not at least as restrictive
+ * as what has been set. Note that the current password will remain until the user has set a new
+ * one, so the change does not take place immediately. To prompt the user for a new password,
+ * use {@link #ACTION_SET_NEW_PASSWORD} or {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after
+ * setting this value. This constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired minimum number of letters required in the password. A value of
+ * 0 means there is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordMinimumNonLetter(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumNonLetter(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of non-letter characters required in the password
+ * for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ * This is the same value as set by
+ * {@link #setPasswordMinimumNonLetter(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to
+ * aggregate all admins.
+ * @return The minimum number of letters required in the password.
+ */
+ public int getPasswordMinimumNonLetter(@Nullable ComponentName admin) {
+ return getPasswordMinimumNonLetter(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordMinimumNonLetter(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumNonLetter(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the length of the password
+ * history. After setting this, the user will not be able to enter a new password that is the
+ * same as any password in the history. Note that the current password will remain until the
+ * user has set a new one, so the change does not take place immediately. To prompt the user for
+ * a new password, use {@link #ACTION_SET_NEW_PASSWORD} or
+ * {@link #ACTION_SET_NEW_PARENT_PROFILE_PASSWORD} after setting this value. This constraint is
+ * only imposed if the administrator has also requested either {@link #PASSWORD_QUALITY_NUMERIC}
+ * , {@link #PASSWORD_QUALITY_NUMERIC_COMPLEX} {@link #PASSWORD_QUALITY_ALPHABETIC}, or
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param length The new desired length of password history. A value of 0 means there is no
+ * restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ */
+ public void setPasswordHistoryLength(@NonNull ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordHistoryLength(admin, length, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a device admin to set the password expiration timeout. Calling this method will
+ * restart the countdown for password expiration for the given admin, as will changing the
+ * device password (for all admins).
+ * <p>
+ * The provided timeout is the time delta in ms and will be added to the current time. For
+ * example, to have the password expire 5 days from now, timeout would be 5 * 86400 * 1000 =
+ * 432000000 ms for timeout.
+ * <p>
+ * To disable password expiration, a value of 0 may be used for timeout.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_EXPIRE_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * Note that setting the password will automatically reset the expiration time for all active
+ * admins. Active admins do not need to explicitly call this method in that case.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param timeout The limit (in ms) that a password can remain in effect. A value of 0 means
+ * there is no restriction (unlimited).
+ * @throws SecurityException if {@code admin} is not an active administrator or {@code admin}
+ * does not use {@link DeviceAdminInfo#USES_POLICY_EXPIRE_PASSWORD}
+ */
+ public void setPasswordExpirationTimeout(@NonNull ComponentName admin, long timeout) {
+ if (mService != null) {
+ try {
+ mService.setPasswordExpirationTimeout(admin, timeout, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Get the password expiration timeout for the given admin. The expiration timeout is the
+ * recurring expiration timeout provided in the call to
+ * {@link #setPasswordExpirationTimeout(ComponentName, long)} for the given admin or the
+ * aggregate of all participating policy administrators if {@code admin} is null. Admins that
+ * have set restrictions on profiles that have a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate all admins.
+ * @return The timeout for the given admin or the minimum of all timeouts
+ */
+ public long getPasswordExpirationTimeout(@Nullable ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordExpirationTimeout(admin, myUserId(), mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Get the current password expiration time for a particular admin or all admins that set
+ * restrictions on this user and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account. If admin is {@code null}, then a composite
+ * of all expiration times is returned - which will be the minimum of all of them.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * the password expiration for the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate all admins.
+ * @return The password expiration time, in milliseconds since epoch.
+ */
+ public long getPasswordExpiration(@Nullable ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordExpiration(admin, myUserId(), mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the current password history length for a particular admin or all admins that
+ * set restrictions on this user and its participating profiles. Restrictions on profiles that
+ * have a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * all admins.
+ * @return The length of the password history
+ */
+ public int getPasswordHistoryLength(@Nullable ComponentName admin) {
+ return getPasswordHistoryLength(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getPasswordHistoryLength(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordHistoryLength(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Return the maximum password length that the device supports for a
+ * particular password quality.
+ * @param quality The quality being interrogated.
+ * @return Returns the maximum length that the user can enter.
+ */
+ public int getPasswordMaximumLength(int quality) {
+ // Kind-of arbitrary.
+ return 16;
+ }
+
+ /**
+ * Determine whether the current password the user has set is sufficient to meet the policy
+ * requirements (e.g. quality, minimum length) that have been requested by the admins of this
+ * user and its participating profiles. Restrictions on profiles that have a separate challenge
+ * are not taken into account. The user must be unlocked in order to perform the check.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to determine if the password set on
+ * the parent profile is sufficient.
+ *
+ * @return Returns true if the password meets the current requirements, else false.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD}
+ * @throws IllegalStateException if the user is not unlocked.
+ */
+ public boolean isActivePasswordSufficient() {
+ if (mService != null) {
+ try {
+ return mService.isActivePasswordSufficient(myUserId(), mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether the current profile password the user has set is sufficient
+ * to meet the policy requirements (e.g. quality, minimum length) that have been
+ * requested by the admins of the parent user and its profiles.
+ *
+ * @param userHandle the userId of the profile to check the password for.
+ * @return Returns true if the password would meet the current requirements, else false.
+ * @throws SecurityException if {@code userHandle} is not a managed profile.
+ * @hide
+ */
+ public boolean isProfileActivePasswordSufficientForParent(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.isProfileActivePasswordSufficientForParent(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the number of times the user has failed at entering a password since that last
+ * successful password entry.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to retrieve the number of failed
+ * password attemts for the parent user.
+ * <p>
+ * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN}
+ * to be able to call this method; if it has not, a security exception will be thrown.
+ *
+ * @return The number of times user has entered an incorrect password since the last correct
+ * password entry.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN}
+ */
+ public int getCurrentFailedPasswordAttempts() {
+ return getCurrentFailedPasswordAttempts(myUserId());
+ }
+
+ /**
+ * Retrieve the number of times the given user has failed at entering a
+ * password since that last successful password entry.
+ *
+ * <p>The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to be able to call this method; if it has
+ * not and it is not the system uid, a security exception will be thrown.
+ *
+ * @hide
+ */
+ public int getCurrentFailedPasswordAttempts(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getCurrentFailedPasswordAttempts(userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Queries whether {@link #RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT} flag is set.
+ *
+ * @return true if RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT flag is set.
+ * @hide
+ */
+ public boolean getDoNotAskCredentialsOnBoot() {
+ if (mService != null) {
+ try {
+ return mService.getDoNotAskCredentialsOnBoot();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Setting this to a value greater than zero enables a built-in policy that will perform a
+ * device or profile wipe after too many incorrect device-unlock passwords have been entered.
+ * This built-in policy combines watching for failed passwords and wiping the device, and
+ * requires that you request both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and
+ * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}}.
+ * <p>
+ * To implement any other policy (e.g. wiping data for a particular application only, erasing or
+ * revoking credentials, or reporting the failure to a server), you should implement
+ * {@link DeviceAdminReceiver#onPasswordFailed(Context, android.content.Intent)} instead. Do not
+ * use this API, because if the maximum count is reached, the device or profile will be wiped
+ * immediately, and your callback will not be invoked.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set a value on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param num The number of failed password attempts at which point the device or profile will
+ * be wiped.
+ * @throws SecurityException if {@code admin} is not an active administrator or does not use
+ * both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and
+ * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}.
+ */
+ public void setMaximumFailedPasswordsForWipe(@NonNull ComponentName admin, int num) {
+ if (mService != null) {
+ try {
+ mService.setMaximumFailedPasswordsForWipe(admin, num, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current maximum number of login attempts that are allowed before the device
+ * or profile is wiped, for a particular admin or all admins that set restrictions on this user
+ * and its participating profiles. Restrictions on profiles that have a separate challenge are
+ * not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * the value for the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * all admins.
+ */
+ public int getMaximumFailedPasswordsForWipe(@Nullable ComponentName admin) {
+ return getMaximumFailedPasswordsForWipe(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getMaximumFailedPasswordsForWipe(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getMaximumFailedPasswordsForWipe(
+ admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the profile with the smallest maximum failed passwords for wipe,
+ * for the given user. So for primary user, it might return the primary or
+ * a managed profile. For a secondary user, it would be the same as the
+ * user passed in.
+ * @hide Used only by Keyguard
+ */
+ public int getProfileWithMinimumFailedPasswordsForWipe(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getProfileWithMinimumFailedPasswordsForWipe(
+ userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return UserHandle.USER_NULL;
+ }
+
+ /**
+ * Flag for {@link #resetPasswordWithToken} and {@link #resetPassword}: don't allow other admins
+ * to change the password again until the user has entered it.
+ */
+ public static final int RESET_PASSWORD_REQUIRE_ENTRY = 0x0001;
+
+ /**
+ * Flag for {@link #resetPasswordWithToken} and {@link #resetPassword}: don't ask for user
+ * credentials on device boot.
+ * If the flag is set, the device can be booted without asking for user password.
+ * The absence of this flag does not change the current boot requirements. This flag
+ * can be set by the device owner only. If the app is not the device owner, the flag
+ * is ignored. Once the flag is set, it cannot be reverted back without resetting the
+ * device to factory defaults.
+ */
+ public static final int RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT = 0x0002;
+
+ /**
+ * Force a new password for device unlock (the password needed to access the entire device) or
+ * the work profile challenge on the current user. This takes effect immediately.
+ * <p>
+ * <em>For device owner and profile owners targeting SDK level
+ * {@link android.os.Build.VERSION_CODES#O} or above, this API is no longer available and will
+ * throw {@link SecurityException}. Please use the new API {@link #resetPasswordWithToken}
+ * instead. </em>
+ * <p>
+ * <em>Note: This API has been limited as of {@link android.os.Build.VERSION_CODES#N} for
+ * device admins that are not device owner and not profile owner.
+ * The password can now only be changed if there is currently no password set. Device owner
+ * and profile owner can still do this when user is unlocked and does not have a managed
+ * profile.</em>
+ * <p>
+ * The given password must be sufficient for the current password quality and length constraints
+ * as returned by {@link #getPasswordQuality(ComponentName)} and
+ * {@link #getPasswordMinimumLength(ComponentName)}; if it does not meet these constraints, then
+ * it will be rejected and false returned. Note that the password may be a stronger quality
+ * (containing alphanumeric characters when the requested quality is only numeric), in which
+ * case the currently active quality will be increased to match.
+ * <p>
+ * Calling with a null or empty password will clear any existing PIN, pattern or password if the
+ * current password constraints allow it. <em>Note: This will not work in
+ * {@link android.os.Build.VERSION_CODES#N} and later for managed profiles, or for device admins
+ * that are not device owner or profile owner. Once set, the password cannot be changed to null
+ * or empty except by these admins.</em>
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ *
+ * @param password The new password for the user. Null or empty clears the password.
+ * @param flags May be 0 or combination of {@link #RESET_PASSWORD_REQUIRE_ENTRY} and
+ * {@link #RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT}.
+ * @return Returns true if the password was applied, or false if it is not acceptable for the
+ * current constraints or if the user has not been decrypted yet.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD}
+ * @throws IllegalStateException if the calling user is locked or has a managed profile.
+ */
+ public boolean resetPassword(String password, int flags) {
+ throwIfParentInstance("resetPassword");
+ if (mService != null) {
+ try {
+ return mService.resetPassword(password, flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a profile or device owner to provision a token which can later be used to reset the
+ * device lockscreen password (if called by device owner), or managed profile challenge (if
+ * called by profile owner), via {@link #resetPasswordWithToken}.
+ * <p>
+ * If the user currently has a lockscreen password, the provisioned token will not be
+ * immediately usable; it only becomes active after the user performs a confirm credential
+ * operation, which can be triggered by {@link KeyguardManager#createConfirmDeviceCredentialIntent}.
+ * If the user has no lockscreen password, the token is activated immediately. In all cases,
+ * the active state of the current token can be checked by {@link #isResetPasswordTokenActive}.
+ * For security reasons, un-activated tokens are only stored in memory and will be lost once
+ * the device reboots. In this case a new token needs to be provisioned again.
+ * <p>
+ * Once provisioned and activated, the token will remain effective even if the user changes
+ * or clears the lockscreen password.
+ * <p>
+ * <em>This token is highly sensitive and should be treated at the same level as user
+ * credentials. In particular, NEVER store this token on device in plaintext. Do not store
+ * the plaintext token in device-encrypted storage if it will be needed to reset password on
+ * file-based encryption devices before user unlocks. Consider carefully how any password token
+ * will be stored on your server and who will need access to them. Tokens may be the subject of
+ * legal access requests.
+ * </em>
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param token a secure token a least 32-byte long, which must be generated by a
+ * cryptographically strong random number generator.
+ * @return true if the operation is successful, false otherwise.
+ * @throws SecurityException if admin is not a device or profile owner.
+ * @throws IllegalArgumentException if the supplied token is invalid.
+ */
+ public boolean setResetPasswordToken(ComponentName admin, byte[] token) {
+ throwIfParentInstance("setResetPasswordToken");
+ if (mService != null) {
+ try {
+ return mService.setResetPasswordToken(admin, token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a profile or device owner to revoke the current password reset token.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return true if the operation is successful, false otherwise.
+ * @throws SecurityException if admin is not a device or profile owner.
+ */
+ public boolean clearResetPasswordToken(ComponentName admin) {
+ throwIfParentInstance("clearResetPasswordToken");
+ if (mService != null) {
+ try {
+ return mService.clearResetPasswordToken(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a profile or device owner to check if the current reset password token is active.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return true if the token is active, false otherwise.
+ * @throws SecurityException if admin is not a device or profile owner.
+ * @throws IllegalStateException if no token has been set.
+ */
+ public boolean isResetPasswordTokenActive(ComponentName admin) {
+ throwIfParentInstance("isResetPasswordTokenActive");
+ if (mService != null) {
+ try {
+ return mService.isResetPasswordTokenActive(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device or profile owner to force set a new device unlock password or a managed
+ * profile challenge on current user. This takes effect immediately.
+ * <p>
+ * Unlike {@link #resetPassword}, this API can change the password even before the user or
+ * device is unlocked or decrypted. The supplied token must have been previously provisioned via
+ * {@link #setResetPasswordToken}, and in active state {@link #isResetPasswordTokenActive}.
+ * <p>
+ * The given password must be sufficient for the current password quality and length constraints
+ * as returned by {@link #getPasswordQuality(ComponentName)} and
+ * {@link #getPasswordMinimumLength(ComponentName)}; if it does not meet these constraints, then
+ * it will be rejected and false returned. Note that the password may be a stronger quality, for
+ * example, a password containing alphanumeric characters when the requested quality is only
+ * numeric.
+ * <p>
+ * Calling with a {@code null} or empty password will clear any existing PIN, pattern or
+ * password if the current password constraints allow it.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param password The new password for the user. {@code null} or empty clears the password.
+ * @param token the password reset token previously provisioned by
+ * {@link #setResetPasswordToken}.
+ * @param flags May be 0 or combination of {@link #RESET_PASSWORD_REQUIRE_ENTRY} and
+ * {@link #RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT}.
+ * @return Returns true if the password was applied, or false if it is not acceptable for the
+ * current constraints.
+ * @throws SecurityException if admin is not a device or profile owner.
+ * @throws IllegalStateException if the provided token is not valid.
+ */
+ public boolean resetPasswordWithToken(@NonNull ComponentName admin, String password,
+ byte[] token, int flags) {
+ throwIfParentInstance("resetPassword");
+ if (mService != null) {
+ try {
+ return mService.resetPasswordWithToken(admin, password, token, flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the maximum time for user
+ * activity until the device will lock. This limits the length that the user can set. It takes
+ * effect immediately.
+ * <p>
+ * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
+ * to be able to call this method; if it has not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param timeMs The new desired maximum time to lock in milliseconds. A value of 0 means there
+ * is no restriction.
+ * @throws SecurityException if {@code admin} is not an active administrator or it does not use
+ * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
+ */
+ public void setMaximumTimeToLock(@NonNull ComponentName admin, long timeMs) {
+ if (mService != null) {
+ try {
+ mService.setMaximumTimeToLock(admin, timeMs, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current maximum time to unlock for a particular admin or all admins that set
+ * restrictions on this user and its participating profiles. Restrictions on profiles that have
+ * a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * all admins.
+ * @return time in milliseconds for the given admin or the minimum value (strictest) of
+ * all admins if admin is null. Returns 0 if there are no restrictions.
+ */
+ public long getMaximumTimeToLock(@Nullable ComponentName admin) {
+ return getMaximumTimeToLock(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public long getMaximumTimeToLock(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getMaximumTimeToLock(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Returns maximum time to lock that applied by all profiles in this user. We do this because we
+ * do not have a separate timeout to lock for work challenge only.
+ *
+ * @hide
+ */
+ public long getMaximumTimeToLockForUserAndProfiles(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getMaximumTimeToLockForUserAndProfiles(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by a device/profile owner to set the timeout after which unlocking with secondary, non
+ * strong auth (e.g. fingerprint, trust agents) times out, i.e. the user has to use a strong
+ * authentication method like password, pin or pattern.
+ *
+ * <p>This timeout is used internally to reset the timer to require strong auth again after
+ * specified timeout each time it has been successfully used.
+ *
+ * <p>Fingerprint can also be disabled altogether using {@link #KEYGUARD_DISABLE_FINGERPRINT}.
+ *
+ * <p>Trust agents can also be disabled altogether using {@link #KEYGUARD_DISABLE_TRUST_AGENTS}.
+ *
+ * <p>The calling device admin must be a device or profile owner. If it is not,
+ * a {@link SecurityException} will be thrown.
+ *
+ * <p>The calling device admin can verify the value it has set by calling
+ * {@link #getRequiredStrongAuthTimeout(ComponentName)} and passing in its instance.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param timeoutMs The new timeout in milliseconds, after which the user will have to unlock
+ * with strong authentication method. A value of 0 means the admin is not participating
+ * in controlling the timeout.
+ * The minimum and maximum timeouts are platform-defined and are typically 1 hour and
+ * 72 hours, respectively. Though discouraged, the admin may choose to require strong
+ * auth at all times using {@link #KEYGUARD_DISABLE_FINGERPRINT} and/or
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS}.
+ *
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setRequiredStrongAuthTimeout(@NonNull ComponentName admin,
+ long timeoutMs) {
+ if (mService != null) {
+ try {
+ mService.setRequiredStrongAuthTimeout(admin, timeoutMs, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Determine for how long the user will be able to use secondary, non strong auth for
+ * authentication, since last strong method authentication (password, pin or pattern) was used.
+ * After the returned timeout the user is required to use strong authentication method.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to aggregate
+ * accross all participating admins.
+ * @return The timeout in milliseconds or 0 if not configured for the provided admin.
+ */
+ public long getRequiredStrongAuthTimeout(@Nullable ComponentName admin) {
+ return getRequiredStrongAuthTimeout(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public long getRequiredStrongAuthTimeout(@Nullable ComponentName admin, @UserIdInt int userId) {
+ if (mService != null) {
+ try {
+ return mService.getRequiredStrongAuthTimeout(admin, userId, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return DEFAULT_STRONG_AUTH_TIMEOUT_MS;
+ }
+
+ /**
+ * Flag for {@link #lockNow(int)}: also evict the user's credential encryption key from the
+ * keyring. The user's credential will need to be entered again in order to derive the
+ * credential encryption key that will be stored back in the keyring for future use.
+ * <p>
+ * This flag can only be used by a profile owner when locking a managed profile when
+ * {@link #getStorageEncryptionStatus} returns {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+ * <p>
+ * In order to secure user data, the user will be stopped and restarted so apps should wait
+ * until they are next run to perform further actions.
+ */
+ public static final int FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY = 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value={FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY})
+ public @interface LockNowFlag {}
+
+ /**
+ * Make the device lock immediately, as if the lock screen timeout has expired at the point of
+ * this call.
+ * <p>
+ * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
+ * to be able to call this method; if it has not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to lock the parent profile.
+ * <p>
+ * Equivalent to calling {@link #lockNow(int)} with no flags.
+ *
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
+ */
+ public void lockNow() {
+ lockNow(0);
+ }
+
+ /**
+ * Make the device lock immediately, as if the lock screen timeout has expired at the point of
+ * this call.
+ * <p>
+ * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK}
+ * to be able to call this method; if it has not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to lock the parent profile.
+ *
+ * @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} or the
+ * {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an application
+ * that is not a profile
+ * owner of a managed profile.
+ * @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is
+ * passed when locking the parent profile.
+ * @throws UnsupportedOperationException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}
+ * flag is passed when {@link #getStorageEncryptionStatus} does not return
+ * {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+ */
+ public void lockNow(@LockNowFlag int flags) {
+ if (mService != null) {
+ try {
+ mService.lockNow(flags, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Flag for {@link #wipeData(int)}: also erase the device's external
+ * storage (such as SD cards).
+ */
+ public static final int WIPE_EXTERNAL_STORAGE = 0x0001;
+
+ /**
+ * Flag for {@link #wipeData(int)}: also erase the factory reset protection
+ * data.
+ *
+ * <p>This flag may only be set by device owner admins; if it is set by
+ * other admins a {@link SecurityException} will be thrown.
+ */
+ public static final int WIPE_RESET_PROTECTION_DATA = 0x0002;
+
+ /**
+ * Flag for {@link #wipeData(int)}: also erase the device's eUICC data.
+ *
+ * TODO(b/35851809): make this public.
+ * @hide
+ */
+ public static final int WIPE_EUICC = 0x0004;
+
+ /**
+ * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
+ * other users will remain unaffected. Calling from the primary user will cause the device to
+ * reboot, erasing all device data - including all the secondary users and their data - while
+ * booting up.
+ * <p>
+ * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
+ * be able to call this method; if it has not, a security exception will be thrown.
+ *
+ * @param flags Bit mask of additional options: currently supported flags are
+ * {@link #WIPE_EXTERNAL_STORAGE} and {@link #WIPE_RESET_PROTECTION_DATA}.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}
+ */
+ public void wipeData(int flags) {
+ throwIfParentInstance("wipeData");
+ if (mService != null) {
+ try {
+ mService.wipeData(flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * global proxy and exclusion list.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_SETS_GLOBAL_PROXY} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ * Only the first device admin can set the proxy. If a second admin attempts
+ * to set the proxy, the {@link ComponentName} of the admin originally setting the
+ * proxy will be returned. If successful in setting the proxy, {@code null} will
+ * be returned.
+ * The method can be called repeatedly by the device admin alrady setting the
+ * proxy to update the proxy and exclusion list.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param proxySpec the global proxy desired. Must be an HTTP Proxy.
+ * Pass Proxy.NO_PROXY to reset the proxy.
+ * @param exclusionList a list of domains to be excluded from the global proxy.
+ * @return {@code null} if the proxy was successfully set, or otherwise a {@link ComponentName}
+ * of the device admin that sets the proxy.
+ * @hide
+ */
+ public @Nullable ComponentName setGlobalProxy(@NonNull ComponentName admin, Proxy proxySpec,
+ List<String> exclusionList ) {
+ throwIfParentInstance("setGlobalProxy");
+ if (proxySpec == null) {
+ throw new NullPointerException();
+ }
+ if (mService != null) {
+ try {
+ String hostSpec;
+ String exclSpec;
+ if (proxySpec.equals(Proxy.NO_PROXY)) {
+ hostSpec = null;
+ exclSpec = null;
+ } else {
+ if (!proxySpec.type().equals(Proxy.Type.HTTP)) {
+ throw new IllegalArgumentException();
+ }
+ InetSocketAddress sa = (InetSocketAddress)proxySpec.address();
+ String hostName = sa.getHostName();
+ int port = sa.getPort();
+ StringBuilder hostBuilder = new StringBuilder();
+ hostSpec = hostBuilder.append(hostName)
+ .append(":").append(Integer.toString(port)).toString();
+ if (exclusionList == null) {
+ exclSpec = "";
+ } else {
+ StringBuilder listBuilder = new StringBuilder();
+ boolean firstDomain = true;
+ for (String exclDomain : exclusionList) {
+ if (!firstDomain) {
+ listBuilder = listBuilder.append(",");
+ } else {
+ firstDomain = false;
+ }
+ listBuilder = listBuilder.append(exclDomain.trim());
+ }
+ exclSpec = listBuilder.toString();
+ }
+ if (android.net.Proxy.validate(hostName, Integer.toString(port), exclSpec)
+ != android.net.Proxy.PROXY_VALID)
+ throw new IllegalArgumentException();
+ }
+ return mService.setGlobalProxy(admin, hostSpec, exclSpec);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set a network-independent global HTTP proxy. This is not normally what you want for typical
+ * HTTP proxies - they are generally network dependent. However if you're doing something
+ * unusual like general internal filtering this may be useful. On a private network where the
+ * proxy is not accessible, you may break HTTP using this.
+ * <p>
+ * This method requires the caller to be the device owner.
+ * <p>
+ * This proxy is only a recommendation and it is possible that some apps will ignore it.
+ *
+ * @see ProxyInfo
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param proxyInfo The a {@link ProxyInfo} object defining the new global HTTP proxy. A
+ * {@code null} value will clear the global HTTP proxy.
+ * @throws SecurityException if {@code admin} is not the device owner.
+ */
+ public void setRecommendedGlobalProxy(@NonNull ComponentName admin, @Nullable ProxyInfo
+ proxyInfo) {
+ throwIfParentInstance("setRecommendedGlobalProxy");
+ if (mService != null) {
+ try {
+ mService.setRecommendedGlobalProxy(admin, proxyInfo);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the component name setting the global proxy.
+ * @return ComponentName object of the device admin that set the global proxy, or {@code null}
+ * if no admin has set the proxy.
+ * @hide
+ */
+ public @Nullable ComponentName getGlobalProxyAdmin() {
+ if (mService != null) {
+ try {
+ return mService.getGlobalProxyAdmin(myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Result code for {@link #setStorageEncryption} and {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is not supported.
+ */
+ public static final int ENCRYPTION_STATUS_UNSUPPORTED = 0;
+
+ /**
+ * Result code for {@link #setStorageEncryption} and {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is supported, but is not currently active.
+ */
+ public static final int ENCRYPTION_STATUS_INACTIVE = 1;
+
+ /**
+ * Result code for {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is not currently active, but is currently
+ * being activated. This is only reported by devices that support
+ * encryption of data and only when the storage is currently
+ * undergoing a process of becoming encrypted. A device that must reboot and/or wipe data
+ * to become encrypted will never return this value.
+ */
+ public static final int ENCRYPTION_STATUS_ACTIVATING = 2;
+
+ /**
+ * Result code for {@link #setStorageEncryption} and {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is active.
+ * <p>
+ * Also see {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+ */
+ public static final int ENCRYPTION_STATUS_ACTIVE = 3;
+
+ /**
+ * Result code for {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is active, but an encryption key has not
+ * been set by the user.
+ */
+ public static final int ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY = 4;
+
+ /**
+ * Result code for {@link #getStorageEncryptionStatus}:
+ * indicating that encryption is active and the encryption key is tied to the user or profile.
+ * <p>
+ * This value is only returned to apps targeting API level 24 and above. For apps targeting
+ * earlier API levels, {@link #ENCRYPTION_STATUS_ACTIVE} is returned, even if the
+ * encryption key is specific to the user or profile.
+ */
+ public static final int ENCRYPTION_STATUS_ACTIVE_PER_USER = 5;
+
+ /**
+ * Activity action: begin the process of encrypting data on the device. This activity should
+ * be launched after using {@link #setStorageEncryption} to request encryption be activated.
+ * After resuming from this activity, use {@link #getStorageEncryption}
+ * to check encryption status. However, on some devices this activity may never return, as
+ * it may trigger a reboot and in some cases a complete data wipe of the device.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_START_ENCRYPTION
+ = "android.app.action.START_ENCRYPTION";
+ /**
+ * Widgets are enabled in keyguard
+ */
+ public static final int KEYGUARD_DISABLE_FEATURES_NONE = 0;
+
+ /**
+ * Disable all keyguard widgets. Has no effect.
+ */
+ public static final int KEYGUARD_DISABLE_WIDGETS_ALL = 1 << 0;
+
+ /**
+ * Disable the camera on secure keyguard screens (e.g. PIN/Pattern/Password)
+ */
+ public static final int KEYGUARD_DISABLE_SECURE_CAMERA = 1 << 1;
+
+ /**
+ * Disable showing all notifications on secure keyguard screens (e.g. PIN/Pattern/Password)
+ */
+ public static final int KEYGUARD_DISABLE_SECURE_NOTIFICATIONS = 1 << 2;
+
+ /**
+ * Only allow redacted notifications on secure keyguard screens (e.g. PIN/Pattern/Password)
+ */
+ public static final int KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS = 1 << 3;
+
+ /**
+ * Ignore trust agent state on secure keyguard screens
+ * (e.g. PIN/Pattern/Password).
+ */
+ public static final int KEYGUARD_DISABLE_TRUST_AGENTS = 1 << 4;
+
+ /**
+ * Disable fingerprint sensor on keyguard secure screens (e.g. PIN/Pattern/Password).
+ */
+ public static final int KEYGUARD_DISABLE_FINGERPRINT = 1 << 5;
+
+ /**
+ * Disable text entry into notifications on secure keyguard screens (e.g. PIN/Pattern/Password).
+ */
+ public static final int KEYGUARD_DISABLE_REMOTE_INPUT = 1 << 6;
+
+ /**
+ * Disable all current and future keyguard customizations.
+ */
+ public static final int KEYGUARD_DISABLE_FEATURES_ALL = 0x7fffffff;
+
+ /**
+ * Keyguard features that when set on a managed profile that doesn't have its own challenge will
+ * affect the profile's parent user. These can also be set on the managed profile's parent
+ * {@link DevicePolicyManager} instance.
+ *
+ * @hide
+ */
+ public static final int PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER =
+ DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS
+ | DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT;
+
+ /**
+ * Called by an application that is administering the device to request that the storage system
+ * be encrypted.
+ * <p>
+ * When multiple device administrators attempt to control device encryption, the most secure,
+ * supported setting will always be used. If any device administrator requests device
+ * encryption, it will be enabled; Conversely, if a device administrator attempts to disable
+ * device encryption while another device administrator has enabled it, the call to disable will
+ * fail (most commonly returning {@link #ENCRYPTION_STATUS_ACTIVE}).
+ * <p>
+ * This policy controls encryption of the secure (application data) storage area. Data written
+ * to other storage areas may or may not be encrypted, and this policy does not require or
+ * control the encryption of any other storage areas. There is one exception: If
+ * {@link android.os.Environment#isExternalStorageEmulated()} is {@code true}, then the
+ * directory returned by {@link android.os.Environment#getExternalStorageDirectory()} must be
+ * written to disk within the encrypted storage area.
+ * <p>
+ * Important Note: On some devices, it is possible to encrypt storage without requiring the user
+ * to create a device PIN or Password. In this case, the storage is encrypted, but the
+ * encryption key may not be fully secured. For maximum security, the administrator should also
+ * require (and check for) a pattern, PIN, or password.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param encrypt true to request encryption, false to release any previous request
+ * @return the new request status (for all active admins) - will be one of
+ * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, or
+ * {@link #ENCRYPTION_STATUS_ACTIVE}. This is the value of the requests; Use
+ * {@link #getStorageEncryptionStatus()} to query the actual device state.
+ * @throws SecurityException if {@code admin} is not an active administrator or does not use
+ * {@link DeviceAdminInfo#USES_ENCRYPTED_STORAGE}
+ */
+ public int setStorageEncryption(@NonNull ComponentName admin, boolean encrypt) {
+ throwIfParentInstance("setStorageEncryption");
+ if (mService != null) {
+ try {
+ return mService.setStorageEncryption(admin, encrypt);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return ENCRYPTION_STATUS_UNSUPPORTED;
+ }
+
+ /**
+ * Called by an application that is administering the device to
+ * determine the requested setting for secure storage.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. If null,
+ * this will return the requested encryption setting as an aggregate of all active
+ * administrators.
+ * @return true if the admin(s) are requesting encryption, false if not.
+ */
+ public boolean getStorageEncryption(@Nullable ComponentName admin) {
+ throwIfParentInstance("getStorageEncryption");
+ if (mService != null) {
+ try {
+ return mService.getStorageEncryption(admin, myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by an application that is administering the device to
+ * determine the current encryption status of the device.
+ * <p>
+ * Depending on the returned status code, the caller may proceed in different
+ * ways. If the result is {@link #ENCRYPTION_STATUS_UNSUPPORTED}, the
+ * storage system does not support encryption. If the
+ * result is {@link #ENCRYPTION_STATUS_INACTIVE}, use {@link
+ * #ACTION_START_ENCRYPTION} to begin the process of encrypting or decrypting the
+ * storage. If the result is {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, the
+ * storage system has enabled encryption but no password is set so further action
+ * may be required. If the result is {@link #ENCRYPTION_STATUS_ACTIVATING},
+ * {@link #ENCRYPTION_STATUS_ACTIVE} or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER},
+ * no further action is required.
+ *
+ * @return current status of encryption. The value will be one of
+ * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE},
+ * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY},
+ * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+ */
+ public int getStorageEncryptionStatus() {
+ throwIfParentInstance("getStorageEncryptionStatus");
+ return getStorageEncryptionStatus(myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getStorageEncryptionStatus(int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getStorageEncryptionStatus(mContext.getPackageName(), userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return ENCRYPTION_STATUS_UNSUPPORTED;
+ }
+
+ /**
+ * Mark a CA certificate as approved by the device user. This means that they have been notified
+ * of the installation, were made aware of the risks, viewed the certificate and still wanted to
+ * keep the certificate on the device.
+ *
+ * Calling with {@param approval} as {@code true} will cancel any ongoing warnings related to
+ * this certificate.
+ *
+ * @hide
+ */
+ public boolean approveCaCert(String alias, int userHandle, boolean approval) {
+ if (mService != null) {
+ try {
+ return mService.approveCaCert(alias, userHandle, approval);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether a CA certificate has been approved by the device user.
+ *
+ * @hide
+ */
+ public boolean isCaCertApproved(String alias, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.isCaCertApproved(alias, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Installs the given certificate as a user CA.
+ *
+ * The caller must be a profile or device owner on that user, or a delegate package given the
+ * {@link #DELEGATION_CERT_INSTALL} scope via {@link #setDelegatedScopes}; otherwise a
+ * security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param certBuffer encoded form of the certificate to install.
+ *
+ * @return false if the certBuffer cannot be parsed or installation is
+ * interrupted, true otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_CERT_INSTALL
+ */
+ public boolean installCaCert(@Nullable ComponentName admin, byte[] certBuffer) {
+ throwIfParentInstance("installCaCert");
+ if (mService != null) {
+ try {
+ return mService.installCaCert(admin, mContext.getPackageName(), certBuffer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Uninstalls the given certificate from trusted user CAs, if present.
+ *
+ * The caller must be a profile or device owner on that user, or a delegate package given the
+ * {@link #DELEGATION_CERT_INSTALL} scope via {@link #setDelegatedScopes}; otherwise a
+ * security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param certBuffer encoded form of the certificate to remove.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_CERT_INSTALL
+ */
+ public void uninstallCaCert(@Nullable ComponentName admin, byte[] certBuffer) {
+ throwIfParentInstance("uninstallCaCert");
+ if (mService != null) {
+ try {
+ final String alias = getCaCertAlias(certBuffer);
+ mService.uninstallCaCerts(admin, mContext.getPackageName(), new String[] {alias});
+ } catch (CertificateException e) {
+ Log.w(TAG, "Unable to parse certificate", e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns all CA certificates that are currently trusted, excluding system CA certificates.
+ * If a user has installed any certificates by other means than device policy these will be
+ * included too.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @return a List of byte[] arrays, each encoding one user CA certificate.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ */
+ public @NonNull List<byte[]> getInstalledCaCerts(@Nullable ComponentName admin) {
+ final List<byte[]> certs = new ArrayList<byte[]>();
+ throwIfParentInstance("getInstalledCaCerts");
+ if (mService != null) {
+ try {
+ mService.enforceCanManageCaCerts(admin, mContext.getPackageName());
+ final TrustedCertificateStore certStore = new TrustedCertificateStore();
+ for (String alias : certStore.userAliases()) {
+ try {
+ certs.add(certStore.getCertificate(alias).getEncoded());
+ } catch (CertificateException ce) {
+ Log.w(TAG, "Could not encode certificate: " + alias, ce);
+ }
+ }
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return certs;
+ }
+
+ /**
+ * Uninstalls all custom trusted CA certificates from the profile. Certificates installed by
+ * means other than device policy will also be removed, except for system CA certificates.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ */
+ public void uninstallAllUserCaCerts(@Nullable ComponentName admin) {
+ throwIfParentInstance("uninstallAllUserCaCerts");
+ if (mService != null) {
+ try {
+ mService.uninstallCaCerts(admin, mContext.getPackageName(),
+ new TrustedCertificateStore().userAliases() .toArray(new String[0]));
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns whether this certificate is installed as a trusted CA.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param certBuffer encoded form of the certificate to look up.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ */
+ public boolean hasCaCertInstalled(@Nullable ComponentName admin, byte[] certBuffer) {
+ throwIfParentInstance("hasCaCertInstalled");
+ if (mService != null) {
+ try {
+ mService.enforceCanManageCaCerts(admin, mContext.getPackageName());
+ return getCaCertAlias(certBuffer) != null;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ } catch (CertificateException ce) {
+ Log.w(TAG, "Could not parse certificate", ce);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a device or profile owner, or delegated certificate installer, to install a
+ * certificate and corresponding private key. All apps within the profile will be able to access
+ * the certificate and use the private key, given direct user approval.
+ *
+ * <p>Access to the installed credentials will not be granted to the caller of this API without
+ * direct user approval. This is for security - should a certificate installer become
+ * compromised, certificates it had already installed will be protected.
+ *
+ * <p>If the installer must have access to the credentials, call
+ * {@link #installKeyPair(ComponentName, PrivateKey, Certificate[], String, boolean)} instead.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param privKey The private key to install.
+ * @param cert The certificate to install.
+ * @param alias The private key alias under which to install the certificate. If a certificate
+ * with that alias already exists, it will be overwritten.
+ * @return {@code true} if the keys were installed, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_CERT_INSTALL
+ */
+ public boolean installKeyPair(@Nullable ComponentName admin, @NonNull PrivateKey privKey,
+ @NonNull Certificate cert, @NonNull String alias) {
+ return installKeyPair(admin, privKey, new Certificate[] {cert}, alias, false);
+ }
+
+ /**
+ * Called by a device or profile owner, or delegated certificate installer, to install a
+ * certificate chain and corresponding private key for the leaf certificate. All apps within the
+ * profile will be able to access the certificate chain and use the private key, given direct
+ * user approval.
+ *
+ * <p>The caller of this API may grant itself access to the certificate and private key
+ * immediately, without user approval. It is a best practice not to request this unless strictly
+ * necessary since it opens up additional security vulnerabilities.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param privKey The private key to install.
+ * @param certs The certificate chain to install. The chain should start with the leaf
+ * certificate and include the chain of trust in order. This will be returned by
+ * {@link android.security.KeyChain#getCertificateChain}.
+ * @param alias The private key alias under which to install the certificate. If a certificate
+ * with that alias already exists, it will be overwritten.
+ * @param requestAccess {@code true} to request that the calling app be granted access to the
+ * credentials immediately. Otherwise, access to the credentials will be gated by user
+ * approval.
+ * @return {@code true} if the keys were installed, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @see android.security.KeyChain#getCertificateChain
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_CERT_INSTALL
+ */
+ public boolean installKeyPair(@Nullable ComponentName admin, @NonNull PrivateKey privKey,
+ @NonNull Certificate[] certs, @NonNull String alias, boolean requestAccess) {
+ throwIfParentInstance("installKeyPair");
+ try {
+ final byte[] pemCert = Credentials.convertToPem(certs[0]);
+ byte[] pemChain = null;
+ if (certs.length > 1) {
+ pemChain = Credentials.convertToPem(Arrays.copyOfRange(certs, 1, certs.length));
+ }
+ final byte[] pkcs8Key = KeyFactory.getInstance(privKey.getAlgorithm())
+ .getKeySpec(privKey, PKCS8EncodedKeySpec.class).getEncoded();
+ return mService.installKeyPair(admin, mContext.getPackageName(), pkcs8Key, pemCert,
+ pemChain, alias, requestAccess);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ Log.w(TAG, "Failed to obtain private key material", e);
+ } catch (CertificateException | IOException e) {
+ Log.w(TAG, "Could not pem-encode certificate", e);
+ }
+ return false;
+ }
+
+ /**
+ * Called by a device or profile owner, or delegated certificate installer, to remove a
+ * certificate and private key pair installed under a given alias.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param alias The private key alias under which the certificate is installed.
+ * @return {@code true} if the private key alias no longer exists, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_CERT_INSTALL
+ */
+ public boolean removeKeyPair(@Nullable ComponentName admin, @NonNull String alias) {
+ throwIfParentInstance("removeKeyPair");
+ try {
+ return mService.removeKeyPair(admin, mContext.getPackageName(), alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @return the alias of a given CA certificate in the certificate store, or {@code null} if it
+ * doesn't exist.
+ */
+ private static String getCaCertAlias(byte[] certBuffer) throws CertificateException {
+ final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ final X509Certificate cert = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(certBuffer));
+ return new TrustedCertificateStore().getCertificateAlias(cert);
+ }
+
+ /**
+ * Called by a profile owner or device owner to grant access to privileged certificate
+ * manipulation APIs to a third-party certificate installer app. Granted APIs include
+ * {@link #getInstalledCaCerts}, {@link #hasCaCertInstalled}, {@link #installCaCert},
+ * {@link #uninstallCaCert}, {@link #uninstallAllUserCaCerts} and {@link #installKeyPair}.
+ * <p>
+ * Delegated certificate installer is a per-user state. The delegated access is persistent until
+ * it is later cleared by calling this method with a null value or uninstallling the certificate
+ * installer.
+ * <p>
+ * <b>Note:</b>Starting from {@link android.os.Build.VERSION_CODES#N}, if the caller
+ * application's target SDK version is {@link android.os.Build.VERSION_CODES#N} or newer, the
+ * supplied certificate installer package must be installed when calling this API, otherwise an
+ * {@link IllegalArgumentException} will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param installerPackage The package name of the certificate installer which will be given
+ * access. If {@code null} is given the current package will be cleared.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}. Use {@link #setDelegatedScopes}
+ * with the {@link #DELEGATION_CERT_INSTALL} scope instead.
+ */
+ @Deprecated
+ public void setCertInstallerPackage(@NonNull ComponentName admin, @Nullable String
+ installerPackage) throws SecurityException {
+ throwIfParentInstance("setCertInstallerPackage");
+ if (mService != null) {
+ try {
+ mService.setCertInstallerPackage(admin, installerPackage);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner or device owner to retrieve the certificate installer for the user,
+ * or {@code null} if none is set. If there are multiple delegates this function will return one
+ * of them.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The package name of the current delegated certificate installer, or {@code null} if
+ * none is set.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}. Use {@link #getDelegatePackages}
+ * with the {@link #DELEGATION_CERT_INSTALL} scope instead.
+ */
+ @Deprecated
+ public @Nullable String getCertInstallerPackage(@NonNull ComponentName admin)
+ throws SecurityException {
+ throwIfParentInstance("getCertInstallerPackage");
+ if (mService != null) {
+ try {
+ return mService.getCertInstallerPackage(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile owner or device owner to grant access to privileged APIs to another app.
+ * Granted APIs are determined by {@code scopes}, which is a list of the {@code DELEGATION_*}
+ * constants.
+ * <p>
+ * A broadcast with the {@link #ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED} action will be
+ * sent to the {@code delegatePackage} with its new scopes in an {@code ArrayList<String>} extra
+ * under the {@link #EXTRA_DELEGATION_SCOPES} key. The broadcast is sent with the
+ * {@link Intent#FLAG_RECEIVER_REGISTERED_ONLY} flag.
+ * <p>
+ * Delegated scopes are a per-user state. The delegated access is persistent until it is later
+ * cleared by calling this method with an empty {@code scopes} list or uninstalling the
+ * {@code delegatePackage}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param delegatePackage The package name of the app which will be given access.
+ * @param scopes The groups of privileged APIs whose access should be granted to
+ * {@code delegatedPackage}.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ */
+ public void setDelegatedScopes(@NonNull ComponentName admin, @NonNull String delegatePackage,
+ @NonNull List<String> scopes) {
+ throwIfParentInstance("setDelegatedScopes");
+ if (mService != null) {
+ try {
+ mService.setDelegatedScopes(admin, delegatePackage, scopes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner or device owner to retrieve a list of the scopes given to a
+ * delegate package. Other apps can use this method to retrieve their own delegated scopes by
+ * passing {@code null} for {@code admin} and their own package name as
+ * {@code delegatedPackage}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is {@code delegatedPackage}.
+ * @param delegatedPackage The package name of the app whose scopes should be retrieved.
+ * @return A list containing the scopes given to {@code delegatedPackage}.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ */
+ @NonNull
+ public List<String> getDelegatedScopes(@Nullable ComponentName admin,
+ @NonNull String delegatedPackage) {
+ throwIfParentInstance("getDelegatedScopes");
+ if (mService != null) {
+ try {
+ return mService.getDelegatedScopes(admin, delegatedPackage);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile owner or device owner to retrieve a list of delegate packages that were
+ * granted a delegation scope.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param delegationScope The scope whose delegates should be retrieved.
+ * @return A list of package names of the current delegated packages for
+ {@code delegationScope}.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ */
+ @Nullable
+ public List<String> getDelegatePackages(@NonNull ComponentName admin,
+ @NonNull String delegationScope) {
+ throwIfParentInstance("getDelegatePackages");
+ if (mService != null) {
+ try {
+ return mService.getDelegatePackages(admin, delegationScope);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a device or profile owner to configure an always-on VPN connection through a
+ * specific application for the current user. This connection is automatically granted and
+ * persisted after a reboot.
+ * <p>
+ * To support the always-on feature, an app must
+ * <ul>
+ * <li>declare a {@link android.net.VpnService} in its manifest, guarded by
+ * {@link android.Manifest.permission#BIND_VPN_SERVICE};</li>
+ * <li>target {@link android.os.Build.VERSION_CODES#N API 24} or above; and</li>
+ * <li><i>not</i> explicitly opt out of the feature through
+ * {@link android.net.VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON}.</li>
+ * </ul>
+ * The call will fail if called with the package name of an unsupported VPN app.
+ *
+ * @param vpnPackage The package name for an installed VPN app on the device, or {@code null} to
+ * remove an existing always-on VPN configuration.
+ * @param lockdownEnabled {@code true} to disallow networking when the VPN is not connected or
+ * {@code false} otherwise. This carries the risk that any failure of the VPN provider
+ * could break networking for all apps. This has no effect when clearing.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ * @throws NameNotFoundException if {@code vpnPackage} is not installed.
+ * @throws UnsupportedOperationException if {@code vpnPackage} exists but does not support being
+ * set as always-on, or if always-on VPN is not available.
+ */
+ public void setAlwaysOnVpnPackage(@NonNull ComponentName admin, @Nullable String vpnPackage,
+ boolean lockdownEnabled)
+ throws NameNotFoundException, UnsupportedOperationException {
+ throwIfParentInstance("setAlwaysOnVpnPackage");
+ if (mService != null) {
+ try {
+ if (!mService.setAlwaysOnVpnPackage(admin, vpnPackage, lockdownEnabled)) {
+ throw new NameNotFoundException(vpnPackage);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a device or profile owner to read the name of the package administering an
+ * always-on VPN connection for the current user. If there is no such package, or the always-on
+ * VPN is provided by the system instead of by an application, {@code null} will be returned.
+ *
+ * @return Package name of VPN controller responsible for always-on VPN, or {@code null} if none
+ * is set.
+ * @throws SecurityException if {@code admin} is not a device or a profile owner.
+ */
+ public @Nullable String getAlwaysOnVpnPackage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getAlwaysOnVpnPackage");
+ if (mService != null) {
+ try {
+ return mService.getAlwaysOnVpnPackage(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by an application that is administering the device to disable all cameras on the
+ * device, for this user. After setting this, no applications running as this user will be able
+ * to access any cameras on the device.
+ * <p>
+ * If the caller is device owner, then the restriction will be applied to all users.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_CAMERA} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled Whether or not the camera should be disabled.
+ * @throws SecurityException if {@code admin} is not an active administrator or does not use
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_CAMERA}.
+ */
+ public void setCameraDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setCameraDisabled");
+ if (mService != null) {
+ try {
+ mService.setCameraDisabled(admin, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Determine whether or not the device's cameras have been disabled for this user,
+ * either by the calling admin, if specified, or all admins.
+ * @param admin The name of the admin component to check, or {@code null} to check whether any admins
+ * have disabled the camera
+ */
+ public boolean getCameraDisabled(@Nullable ComponentName admin) {
+ throwIfParentInstance("getCameraDisabled");
+ return getCameraDisabled(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public boolean getCameraDisabled(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getCameraDisabled(admin, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a device owner to request a bugreport.
+ * <p>
+ * If the device contains secondary users or profiles, they must be affiliated with the device
+ * owner user. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return {@code true} if the bugreport collection started successfully, or {@code false} if it
+ * wasn't triggered because a previous bugreport operation is still active (either the
+ * bugreport is still running or waiting for the user to share or decline)
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ */
+ public boolean requestBugreport(@NonNull ComponentName admin) {
+ throwIfParentInstance("requestBugreport");
+ if (mService != null) {
+ try {
+ return mService.requestBugreport(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether or not creating a guest user has been disabled for the device
+ *
+ * @hide
+ */
+ public boolean getGuestUserDisabled(@Nullable ComponentName admin) {
+ // Currently guest users can always be created if multi-user is enabled
+ // TODO introduce a policy for guest user creation
+ return false;
+ }
+
+ /**
+ * Called by a device/profile owner to set whether the screen capture is disabled. Disabling
+ * screen capture also prevents the content from being shown on display devices that do not have
+ * a secure video output. See {@link android.view.Display#FLAG_SECURE} for more details about
+ * secure surfaces and secure displays.
+ * <p>
+ * The calling device admin must be a device or profile owner. If it is not, a security
+ * exception will be thrown.
+ * <p>
+ * From version {@link android.os.Build.VERSION_CODES#M} disabling screen capture also blocks
+ * assist requests for all activities of the relevant user.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled Whether screen capture is disabled or not.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setScreenCaptureDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setScreenCaptureDisabled");
+ if (mService != null) {
+ try {
+ mService.setScreenCaptureDisabled(admin, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Determine whether or not screen capture has been disabled by the calling
+ * admin, if specified, or all admins.
+ * @param admin The name of the admin component to check, or {@code null} to check whether any admins
+ * have disabled screen capture.
+ */
+ public boolean getScreenCaptureDisabled(@Nullable ComponentName admin) {
+ throwIfParentInstance("getScreenCaptureDisabled");
+ return getScreenCaptureDisabled(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public boolean getScreenCaptureDisabled(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getScreenCaptureDisabled(admin, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a device or profile owner to set whether auto time is required. If auto time is
+ * required, no user will be able set the date and time and network date and time will be used.
+ * <p>
+ * Note: if auto time is required the user can still manually set the time zone.
+ * <p>
+ * The calling device admin must be a device or profile owner. If it is not, a security
+ * exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param required Whether auto time is set required or not.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setAutoTimeRequired(@NonNull ComponentName admin, boolean required) {
+ throwIfParentInstance("setAutoTimeRequired");
+ if (mService != null) {
+ try {
+ mService.setAutoTimeRequired(admin, required);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @return true if auto time is required.
+ */
+ public boolean getAutoTimeRequired() {
+ throwIfParentInstance("getAutoTimeRequired");
+ if (mService != null) {
+ try {
+ return mService.getAutoTimeRequired();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a device owner to set whether all users created on the device should be ephemeral.
+ * <p>
+ * The system user is exempt from this policy - it is never ephemeral.
+ * <p>
+ * The calling device admin must be the device owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param forceEphemeralUsers If true, all the existing users will be deleted and all
+ * subsequently created users will be ephemeral.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @hide
+ */
+ public void setForceEphemeralUsers(
+ @NonNull ComponentName admin, boolean forceEphemeralUsers) {
+ throwIfParentInstance("setForceEphemeralUsers");
+ if (mService != null) {
+ try {
+ mService.setForceEphemeralUsers(admin, forceEphemeralUsers);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @return true if all users are created ephemeral.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @hide
+ */
+ public boolean getForceEphemeralUsers(@NonNull ComponentName admin) {
+ throwIfParentInstance("getForceEphemeralUsers");
+ if (mService != null) {
+ try {
+ return mService.getForceEphemeralUsers(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by an application that is administering the device to disable keyguard customizations,
+ * such as widgets. After setting this, keyguard features will be disabled according to the
+ * provided feature list.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES} to be able to call this method;
+ * if it has not, a security exception will be thrown.
+ * <p>
+ * Calling this from a managed profile before version {@link android.os.Build.VERSION_CODES#M}
+ * will throw a security exception. From version {@link android.os.Build.VERSION_CODES#M} the
+ * profile owner of a managed profile can set:
+ * <ul>
+ * <li>{@link #KEYGUARD_DISABLE_TRUST_AGENTS}, which affects the parent user, but only if there
+ * is no separate challenge set on the managed profile.
+ * <li>{@link #KEYGUARD_DISABLE_FINGERPRINT} which affects the managed profile challenge if
+ * there is one, or the parent user otherwise.
+ * <li>{@link #KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS} which affects notifications generated
+ * by applications in the managed profile.
+ * </ul>
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} and {@link #KEYGUARD_DISABLE_FINGERPRINT} can also be
+ * set on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ * <p>
+ * Requests to disable other features on a managed profile will be ignored.
+ * <p>
+ * The admin can check which features have been disabled by calling
+ * {@link #getKeyguardDisabledFeatures(ComponentName)}
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param which {@link #KEYGUARD_DISABLE_FEATURES_NONE} (default),
+ * {@link #KEYGUARD_DISABLE_WIDGETS_ALL}, {@link #KEYGUARD_DISABLE_SECURE_CAMERA},
+ * {@link #KEYGUARD_DISABLE_SECURE_NOTIFICATIONS},
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS},
+ * {@link #KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS},
+ * {@link #KEYGUARD_DISABLE_FINGERPRINT}, {@link #KEYGUARD_DISABLE_FEATURES_ALL}
+ * @throws SecurityException if {@code admin} is not an active administrator or does not user
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES}
+ */
+ public void setKeyguardDisabledFeatures(@NonNull ComponentName admin, int which) {
+ if (mService != null) {
+ try {
+ mService.setKeyguardDisabledFeatures(admin, which, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Determine whether or not features have been disabled in keyguard either by the calling
+ * admin, if specified, or all admins that set restrictions on this user and its participating
+ * profiles. Restrictions on profiles that have a separate challenge are not taken into account.
+ *
+ * <p>This method can be called on the {@link DevicePolicyManager} instance
+ * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve
+ * restrictions on the parent profile.
+ *
+ * @param admin The name of the admin component to check, or {@code null} to check whether any
+ * admins have disabled features in keyguard.
+ * @return bitfield of flags. See {@link #setKeyguardDisabledFeatures(ComponentName, int)}
+ * for a list.
+ */
+ public int getKeyguardDisabledFeatures(@Nullable ComponentName admin) {
+ return getKeyguardDisabledFeatures(admin, myUserId());
+ }
+
+ /** @hide per-user version */
+ public int getKeyguardDisabledFeatures(@Nullable ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getKeyguardDisabledFeatures(admin, userHandle, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return KEYGUARD_DISABLE_FEATURES_NONE;
+ }
+
+ /**
+ * @hide
+ */
+ public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing,
+ int userHandle) {
+ if (mService != null) {
+ try {
+ mService.setActiveAdmin(policyReceiver, refreshing, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) {
+ setActiveAdmin(policyReceiver, refreshing, myUserId());
+ }
+
+ /**
+ * @hide
+ */
+ public void getRemoveWarning(@Nullable ComponentName admin, RemoteCallback result) {
+ if (mService != null) {
+ try {
+ mService.getRemoveWarning(admin, result, myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void setActivePasswordState(PasswordMetrics metrics, int userHandle) {
+ if (mService != null) {
+ try {
+ mService.setActivePasswordState(metrics, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void reportPasswordChanged(@UserIdInt int userId) {
+ if (mService != null) {
+ try {
+ mService.reportPasswordChanged(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void reportFailedPasswordAttempt(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportFailedPasswordAttempt(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void reportSuccessfulPasswordAttempt(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportSuccessfulPasswordAttempt(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void reportFailedFingerprintAttempt(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportFailedFingerprintAttempt(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void reportSuccessfulFingerprintAttempt(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportSuccessfulFingerprintAttempt(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Should be called when keyguard has been dismissed.
+ * @hide
+ */
+ public void reportKeyguardDismissed(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportKeyguardDismissed(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Should be called when keyguard view has been shown to the user.
+ * @hide
+ */
+ public void reportKeyguardSecured(int userHandle) {
+ if (mService != null) {
+ try {
+ mService.reportKeyguardSecured(userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Sets the given package as the device owner.
+ * Same as {@link #setDeviceOwner(ComponentName, String)} but without setting a device owner name.
+ * @param who the component name to be registered as device owner.
+ * @return whether the package was successfully registered as the device owner.
+ * @throws IllegalArgumentException if the package name is null or invalid
+ * @throws IllegalStateException If the preconditions mentioned are not met.
+ */
+ public boolean setDeviceOwner(ComponentName who) {
+ return setDeviceOwner(who, null);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean setDeviceOwner(ComponentName who, int userId) {
+ return setDeviceOwner(who, null, userId);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean setDeviceOwner(ComponentName who, String ownerName) {
+ return setDeviceOwner(who, ownerName, UserHandle.USER_SYSTEM);
+ }
+
+ /**
+ * @hide
+ * Sets the given package as the device owner. The package must already be installed. There
+ * must not already be a device owner.
+ * Only apps with the MANAGE_PROFILE_AND_DEVICE_OWNERS permission and the shell uid can call
+ * this method.
+ * Calling this after the setup phase of the primary user has completed is allowed only if
+ * the caller is the shell uid, and there are no additional users and no accounts.
+ * @param who the component name to be registered as device owner.
+ * @param ownerName the human readable name of the institution that owns this device.
+ * @param userId ID of the user on which the device owner runs.
+ * @return whether the package was successfully registered as the device owner.
+ * @throws IllegalArgumentException if the package name is null or invalid
+ * @throws IllegalStateException If the preconditions mentioned are not met.
+ */
+ public boolean setDeviceOwner(ComponentName who, String ownerName, int userId)
+ throws IllegalArgumentException, IllegalStateException {
+ if (mService != null) {
+ try {
+ return mService.setDeviceOwner(who, ownerName, userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Used to determine if a particular package has been registered as a Device Owner app.
+ * A device owner app is a special device admin that cannot be deactivated by the user, once
+ * activated as a device admin. It also cannot be uninstalled. To check whether a particular
+ * package is currently registered as the device owner app, pass in the package name from
+ * {@link Context#getPackageName()} to this method.<p/>This is useful for device
+ * admin apps that want to check whether they are also registered as the device owner app. The
+ * exact mechanism by which a device admin app is registered as a device owner app is defined by
+ * the setup process.
+ * @param packageName the package name of the app, to compare with the registered device owner
+ * app, if any.
+ * @return whether or not the package is registered as the device owner app.
+ */
+ public boolean isDeviceOwnerApp(String packageName) {
+ throwIfParentInstance("isDeviceOwnerApp");
+ return isDeviceOwnerAppOnCallingUser(packageName);
+ }
+
+ /**
+ * @return true if a package is registered as device owner, only when it's running on the
+ * calling user.
+ *
+ * <p>Same as {@link #isDeviceOwnerApp}, but bundled code should use it for clarity.
+ * @hide
+ */
+ public boolean isDeviceOwnerAppOnCallingUser(String packageName) {
+ return isDeviceOwnerAppOnAnyUserInner(packageName, /* callingUserOnly =*/ true);
+ }
+
+ /**
+ * @return true if a package is registered as device owner, even if it's running on a different
+ * user.
+ *
+ * <p>Requires the MANAGE_USERS permission.
+ *
+ * @hide
+ */
+ public boolean isDeviceOwnerAppOnAnyUser(String packageName) {
+ return isDeviceOwnerAppOnAnyUserInner(packageName, /* callingUserOnly =*/ false);
+ }
+
+ /**
+ * @return device owner component name, only when it's running on the calling user.
+ *
+ * @hide
+ */
+ public ComponentName getDeviceOwnerComponentOnCallingUser() {
+ return getDeviceOwnerComponentInner(/* callingUserOnly =*/ true);
+ }
+
+ /**
+ * @return device owner component name, even if it's running on a different user.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public ComponentName getDeviceOwnerComponentOnAnyUser() {
+ return getDeviceOwnerComponentInner(/* callingUserOnly =*/ false);
+ }
+
+ private boolean isDeviceOwnerAppOnAnyUserInner(String packageName, boolean callingUserOnly) {
+ if (packageName == null) {
+ return false;
+ }
+ final ComponentName deviceOwner = getDeviceOwnerComponentInner(callingUserOnly);
+ if (deviceOwner == null) {
+ return false;
+ }
+ return packageName.equals(deviceOwner.getPackageName());
+ }
+
+ private ComponentName getDeviceOwnerComponentInner(boolean callingUserOnly) {
+ if (mService != null) {
+ try {
+ return mService.getDeviceOwnerComponent(callingUserOnly);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return ID of the user who runs device owner, or {@link UserHandle#USER_NULL} if there's
+ * no device owner.
+ *
+ * <p>Requires the MANAGE_USERS permission.
+ *
+ * @hide
+ */
+ public int getDeviceOwnerUserId() {
+ if (mService != null) {
+ try {
+ return mService.getDeviceOwnerUserId();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return UserHandle.USER_NULL;
+ }
+
+ /**
+ * Clears the current device owner. The caller must be the device owner. This function should be
+ * used cautiously as once it is called it cannot be undone. The device owner can only be set as
+ * a part of device setup, before it completes.
+ * <p>
+ * While some policies previously set by the device owner will be cleared by this method, it is
+ * a best-effort process and some other policies will still remain in place after the device
+ * owner is cleared.
+ *
+ * @param packageName The package name of the device owner.
+ * @throws SecurityException if the caller is not in {@code packageName} or {@code packageName}
+ * does not own the current device owner component.
+ *
+ * @deprecated This method is expected to be used for testing purposes only. The device owner
+ * will lose control of the device and its data after calling it. In order to protect any
+ * sensitive data that remains on the device, it is advised that the device owner factory resets
+ * the device instead of calling this method. See {@link #wipeData(int)}.
+ */
+ @Deprecated
+ public void clearDeviceOwnerApp(String packageName) {
+ throwIfParentInstance("clearDeviceOwnerApp");
+ if (mService != null) {
+ try {
+ mService.clearDeviceOwner(packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the device owner package name, only if it's running on the calling user.
+ *
+ * <p>Bundled components should use {@code getDeviceOwnerComponentOnCallingUser()} for clarity.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public @Nullable String getDeviceOwner() {
+ throwIfParentInstance("getDeviceOwner");
+ final ComponentName name = getDeviceOwnerComponentOnCallingUser();
+ return name != null ? name.getPackageName() : null;
+ }
+
+ /**
+ * Called by the system to find out whether the device is managed by a Device Owner.
+ *
+ * @return whether the device is managed by a Device Owner.
+ * @throws SecurityException if the caller is not the device owner, does not hold the
+ * MANAGE_USERS permission and is not the system.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ @SuppressLint("Doclava125")
+ public boolean isDeviceManaged() {
+ try {
+ return mService.hasDeviceOwner();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the device owner name. Note this method *will* return the device owner
+ * name when it's running on a different user.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public String getDeviceOwnerNameOnAnyUser() {
+ throwIfParentInstance("getDeviceOwnerNameOnAnyUser");
+ if (mService != null) {
+ try {
+ return mService.getDeviceOwnerName();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ * @deprecated Do not use
+ * @removed
+ */
+ @Deprecated
+ @SystemApi
+ @SuppressLint("Doclava125")
+ public @Nullable String getDeviceInitializerApp() {
+ return null;
+ }
+
+ /**
+ * @hide
+ * @deprecated Do not use
+ * @removed
+ */
+ @Deprecated
+ @SystemApi
+ @SuppressLint("Doclava125")
+ public @Nullable ComponentName getDeviceInitializerComponent() {
+ return null;
+ }
+
+ /**
+ * @hide
+ * @deprecated Use #ACTION_SET_PROFILE_OWNER
+ * Sets the given component as an active admin and registers the package as the profile
+ * owner for this user. The package must already be installed and there shouldn't be
+ * an existing profile owner registered for this user. Also, this method must be called
+ * before the user setup has been completed.
+ * <p>
+ * This method can only be called by system apps that hold MANAGE_USERS permission and
+ * MANAGE_DEVICE_ADMINS permission.
+ * @param admin The component to register as an active admin and profile owner.
+ * @param ownerName The user-visible name of the entity that is managing this user.
+ * @return whether the admin was successfully registered as the profile owner.
+ * @throws IllegalArgumentException if packageName is null, the package isn't installed, or
+ * the user has already been set up.
+ */
+ @Deprecated
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS)
+ public boolean setActiveProfileOwner(@NonNull ComponentName admin, @Deprecated String ownerName)
+ throws IllegalArgumentException {
+ throwIfParentInstance("setActiveProfileOwner");
+ if (mService != null) {
+ try {
+ final int myUserId = myUserId();
+ mService.setActiveAdmin(admin, false, myUserId);
+ return mService.setProfileOwner(admin, ownerName, myUserId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clears the active profile owner. The caller must be the profile owner of this user, otherwise
+ * a SecurityException will be thrown. This method is not available to managed profile owners.
+ * <p>
+ * While some policies previously set by the profile owner will be cleared by this method, it is
+ * a best-effort process and some other policies will still remain in place after the profile
+ * owner is cleared.
+ *
+ * @param admin The component to remove as the profile owner.
+ * @throws SecurityException if {@code admin} is not an active profile owner, or the method is
+ * being called from a managed profile.
+ *
+ * @deprecated This method is expected to be used for testing purposes only. The profile owner
+ * will lose control of the user and its data after calling it. In order to protect any
+ * sensitive data that remains on this user, it is advised that the profile owner deletes it
+ * instead of calling this method. See {@link #wipeData(int)}.
+ */
+ @Deprecated
+ public void clearProfileOwner(@NonNull ComponentName admin) {
+ throwIfParentInstance("clearProfileOwner");
+ if (mService != null) {
+ try {
+ mService.clearProfileOwner(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Checks whether the user was already setup.
+ */
+ public boolean hasUserSetupCompleted() {
+ if (mService != null) {
+ try {
+ return mService.hasUserSetupCompleted();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @hide
+ * Sets the given component as the profile owner of the given user profile. The package must
+ * already be installed. There must not already be a profile owner for this user.
+ * Only apps with the MANAGE_PROFILE_AND_DEVICE_OWNERS permission and the shell uid can call
+ * this method.
+ * Calling this after the setup phase of the specified user has completed is allowed only if:
+ * - the caller is SYSTEM_UID.
+ * - or the caller is the shell uid, and there are no accounts on the specified user.
+ * @param admin the component name to be registered as profile owner.
+ * @param ownerName the human readable name of the organisation associated with this DPM.
+ * @param userHandle the userId to set the profile owner for.
+ * @return whether the component was successfully registered as the profile owner.
+ * @throws IllegalArgumentException if admin is null, the package isn't installed, or the
+ * preconditions mentioned are not met.
+ */
+ public boolean setProfileOwner(@NonNull ComponentName admin, @Deprecated String ownerName,
+ int userHandle) throws IllegalArgumentException {
+ if (mService != null) {
+ try {
+ if (ownerName == null) {
+ ownerName = "";
+ }
+ return mService.setProfileOwner(admin, ownerName, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the device owner information to be shown on the lock screen.
+ * <p>
+ * If the device owner information is {@code null} or empty then the device owner info is
+ * cleared and the user owner info is shown on the lock screen if it is set.
+ * <p>
+ * If the device owner information contains only whitespaces then the message on the lock screen
+ * will be blank and the user will not be allowed to change it.
+ * <p>
+ * If the device owner information needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this string accordingly.
+ *
+ * @param admin The name of the admin component to check.
+ * @param info Device owner information which will be displayed instead of the user owner info.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setDeviceOwnerLockScreenInfo(@NonNull ComponentName admin, CharSequence info) {
+ throwIfParentInstance("setDeviceOwnerLockScreenInfo");
+ if (mService != null) {
+ try {
+ mService.setDeviceOwnerLockScreenInfo(admin, info);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @return The device owner information. If it is not set returns {@code null}.
+ */
+ public CharSequence getDeviceOwnerLockScreenInfo() {
+ throwIfParentInstance("getDeviceOwnerLockScreenInfo");
+ if (mService != null) {
+ try {
+ return mService.getDeviceOwnerLockScreenInfo();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by device or profile owners to suspend packages for this user. This function can be
+ * called by a device owner, profile owner, or by a delegate given the
+ * {@link #DELEGATION_PACKAGE_ACCESS} scope via {@link #setDelegatedScopes}.
+ * <p>
+ * A suspended package will not be able to start activities. Its notifications will be hidden,
+ * it will not show up in recents, will not be able to show toasts or dialogs or ring the
+ * device.
+ * <p>
+ * The package must already be installed. If the package is uninstalled while suspended the
+ * package will no longer be suspended. The admin can block this by using
+ * {@link #setUninstallBlocked}.
+ *
+ * @param admin The name of the admin component to check, or {@code null} if the caller is a
+ * package access delegate.
+ * @param packageNames The package names to suspend or unsuspend.
+ * @param suspended If set to {@code true} than the packages will be suspended, if set to
+ * {@code false} the packages will be unsuspended.
+ * @return an array of package names for which the suspended status is not set as requested in
+ * this method.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public @NonNull String[] setPackagesSuspended(@NonNull ComponentName admin,
+ @NonNull String[] packageNames, boolean suspended) {
+ throwIfParentInstance("setPackagesSuspended");
+ if (mService != null) {
+ try {
+ return mService.setPackagesSuspended(admin, mContext.getPackageName(), packageNames,
+ suspended);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return packageNames;
+ }
+
+ /**
+ * Determine if a package is suspended. This function can be called by a device owner, profile
+ * owner, or by a delegate given the {@link #DELEGATION_PACKAGE_ACCESS} scope via
+ * {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a package access delegate.
+ * @param packageName The name of the package to retrieve the suspended status of.
+ * @return {@code true} if the package is suspended or {@code false} if the package is not
+ * suspended, could not be found or an error occurred.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @throws NameNotFoundException if the package could not be found.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public boolean isPackageSuspended(@NonNull ComponentName admin, String packageName)
+ throws NameNotFoundException {
+ throwIfParentInstance("isPackageSuspended");
+ if (mService != null) {
+ try {
+ return mService.isPackageSuspended(admin, mContext.getPackageName(), packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (IllegalArgumentException ex) {
+ throw new NameNotFoundException(packageName);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the enabled state of the profile. A profile should be enabled only once it is ready to
+ * be used. Only the profile owner can call this.
+ *
+ * @see #isProfileOwnerApp
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public void setProfileEnabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("setProfileEnabled");
+ if (mService != null) {
+ try {
+ mService.setProfileEnabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets the name of the profile. In the device owner case it sets the name of the user which it
+ * is called from. Only a profile owner or device owner can call this. If this is never called
+ * by the profile or device owner, the name will be set to default values.
+ *
+ * @see #isProfileOwnerApp
+ * @see #isDeviceOwnerApp
+ * @param admin Which {@link DeviceAdminReceiver} this request is associate with.
+ * @param profileName The name of the profile.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setProfileName(@NonNull ComponentName admin, String profileName) {
+ throwIfParentInstance("setProfileName");
+ if (mService != null) {
+ try {
+ mService.setProfileName(admin, profileName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Used to determine if a particular package is registered as the profile owner for the
+ * user. A profile owner is a special device admin that has additional privileges
+ * within the profile.
+ *
+ * @param packageName The package name of the app to compare with the registered profile owner.
+ * @return Whether or not the package is registered as the profile owner.
+ */
+ public boolean isProfileOwnerApp(String packageName) {
+ throwIfParentInstance("isProfileOwnerApp");
+ if (mService != null) {
+ try {
+ ComponentName profileOwner = mService.getProfileOwner(myUserId());
+ return profileOwner != null
+ && profileOwner.getPackageName().equals(packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ * @return the packageName of the owner of the given user profile or {@code null} if no profile
+ * owner has been set for that user.
+ * @throws IllegalArgumentException if the userId is invalid.
+ */
+ @SystemApi
+ public @Nullable ComponentName getProfileOwner() throws IllegalArgumentException {
+ throwIfParentInstance("getProfileOwner");
+ return getProfileOwnerAsUser(Process.myUserHandle().getIdentifier());
+ }
+
+ /**
+ * @see #getProfileOwner()
+ * @hide
+ */
+ public @Nullable ComponentName getProfileOwnerAsUser(final int userId)
+ throws IllegalArgumentException {
+ if (mService != null) {
+ try {
+ return mService.getProfileOwner(userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ * @return the human readable name of the organisation associated with this DPM or {@code null}
+ * if one is not set.
+ * @throws IllegalArgumentException if the userId is invalid.
+ */
+ public @Nullable String getProfileOwnerName() throws IllegalArgumentException {
+ if (mService != null) {
+ try {
+ return mService.getProfileOwnerName(Process.myUserHandle().getIdentifier());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ * @param userId The user for whom to fetch the profile owner name, if any.
+ * @return the human readable name of the organisation associated with this profile owner or
+ * null if one is not set.
+ * @throws IllegalArgumentException if the userId is invalid.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public @Nullable String getProfileOwnerNameAsUser(int userId) throws IllegalArgumentException {
+ throwIfParentInstance("getProfileOwnerNameAsUser");
+ if (mService != null) {
+ try {
+ return mService.getProfileOwnerName(userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile owner or device owner to add a default intent handler activity for
+ * intents that match a certain intent filter. This activity will remain the default intent
+ * handler even if the set of potential event handlers for the intent filter changes and if the
+ * intent preferences are reset.
+ * <p>
+ * The default disambiguation mechanism takes over if the activity is not installed (anymore).
+ * When the activity is (re)installed, it is automatically reset as default intent handler for
+ * the filter.
+ * <p>
+ * The calling device admin must be a profile owner or device owner. If it is not, a security
+ * exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param filter The IntentFilter for which a default handler is added.
+ * @param activity The Activity that is added as default intent handler.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void addPersistentPreferredActivity(@NonNull ComponentName admin, IntentFilter filter,
+ @NonNull ComponentName activity) {
+ throwIfParentInstance("addPersistentPreferredActivity");
+ if (mService != null) {
+ try {
+ mService.addPersistentPreferredActivity(admin, filter, activity);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner or device owner to remove all persistent intent handler preferences
+ * associated with the given package that were set by {@link #addPersistentPreferredActivity}.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The name of the package for which preferences are removed.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void clearPackagePersistentPreferredActivities(@NonNull ComponentName admin,
+ String packageName) {
+ throwIfParentInstance("clearPackagePersistentPreferredActivities");
+ if (mService != null) {
+ try {
+ mService.clearPackagePersistentPreferredActivities(admin, packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner or device owner to grant permission to a package to manage
+ * application restrictions for the calling user via {@link #setApplicationRestrictions} and
+ * {@link #getApplicationRestrictions}.
+ * <p>
+ * This permission is persistent until it is later cleared by calling this method with a
+ * {@code null} value or uninstalling the managing package.
+ * <p>
+ * The supplied application restriction managing package must be installed when calling this
+ * API, otherwise an {@link NameNotFoundException} will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The package name which will be given access to application restrictions
+ * APIs. If {@code null} is given the current package will be cleared.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @throws NameNotFoundException if {@code packageName} is not found
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}. Use {@link #setDelegatedScopes}
+ * with the {@link #DELEGATION_APP_RESTRICTIONS} scope instead.
+ */
+ @Deprecated
+ public void setApplicationRestrictionsManagingPackage(@NonNull ComponentName admin,
+ @Nullable String packageName) throws NameNotFoundException {
+ throwIfParentInstance("setApplicationRestrictionsManagingPackage");
+ if (mService != null) {
+ try {
+ if (!mService.setApplicationRestrictionsManagingPackage(admin, packageName)) {
+ throw new NameNotFoundException(packageName);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner or device owner to retrieve the application restrictions managing
+ * package for the current user, or {@code null} if none is set. If there are multiple
+ * delegates this function will return one of them.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The package name allowed to manage application restrictions on the current user, or
+ * {@code null} if none is set.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}. Use {@link #getDelegatePackages}
+ * with the {@link #DELEGATION_APP_RESTRICTIONS} scope instead.
+ */
+ @Deprecated
+ @Nullable
+ public String getApplicationRestrictionsManagingPackage(
+ @NonNull ComponentName admin) {
+ throwIfParentInstance("getApplicationRestrictionsManagingPackage");
+ if (mService != null) {
+ try {
+ return mService.getApplicationRestrictionsManagingPackage(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by any application to find out whether it has been granted permission via
+ * {@link #setApplicationRestrictionsManagingPackage} to manage application restrictions
+ * for the calling user.
+ *
+ * <p>This is done by comparing the calling Linux uid with the uid of the package specified by
+ * that method.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#O}. Use {@link #getDelegatedScopes}
+ * instead.
+ */
+ @Deprecated
+ public boolean isCallerApplicationRestrictionsManagingPackage() {
+ throwIfParentInstance("isCallerApplicationRestrictionsManagingPackage");
+ if (mService != null) {
+ try {
+ return mService.isCallerApplicationRestrictionsManagingPackage(
+ mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the application restrictions for a given target application running in the calling user.
+ * <p>
+ * The caller must be a profile or device owner on that user, or the package allowed to manage
+ * application restrictions via {@link #setDelegatedScopes} with the
+ * {@link #DELEGATION_APP_RESTRICTIONS} scope; otherwise a security exception will be thrown.
+ * <p>
+ * The provided {@link Bundle} consists of key-value pairs, where the types of values may be:
+ * <ul>
+ * <li>{@code boolean}
+ * <li>{@code int}
+ * <li>{@code String} or {@code String[]}
+ * <li>From {@link android.os.Build.VERSION_CODES#M}, {@code Bundle} or {@code Bundle[]}
+ * </ul>
+ * <p>
+ * If the restrictions are not available yet, but may be applied in the near future, the caller
+ * can notify the target application of that by adding
+ * {@link UserManager#KEY_RESTRICTIONS_PENDING} to the settings parameter.
+ * <p>
+ * The application restrictions are only made visible to the target application via
+ * {@link UserManager#getApplicationRestrictions(String)}, in addition to the profile or device
+ * owner, and the application restrictions managing package via
+ * {@link #getApplicationRestrictions}.
+ *
+ * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if called by the application restrictions managing package.
+ * @param packageName The name of the package to update restricted settings for.
+ * @param settings A {@link Bundle} to be parsed by the receiving application, conveying a new
+ * set of active restrictions.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_APP_RESTRICTIONS
+ * @see UserManager#KEY_RESTRICTIONS_PENDING
+ */
+ @WorkerThread
+ public void setApplicationRestrictions(@Nullable ComponentName admin, String packageName,
+ Bundle settings) {
+ throwIfParentInstance("setApplicationRestrictions");
+ if (mService != null) {
+ try {
+ mService.setApplicationRestrictions(admin, mContext.getPackageName(), packageName,
+ settings);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Sets a list of configuration features to enable for a TrustAgent component. This is meant to
+ * be used in conjunction with {@link #KEYGUARD_DISABLE_TRUST_AGENTS}, which disables all trust
+ * agents but those enabled by this function call. If flag
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} is not set, then this call has no effect.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES} to be able to call this method;
+ * if not, a security exception will be thrown.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set the configuration for
+ * the parent profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param target Component name of the agent to be enabled.
+ * @param configuration TrustAgent-specific feature bundle. If null for any admin, agent will be
+ * strictly disabled according to the state of the
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} flag.
+ * <p>
+ * If {@link #KEYGUARD_DISABLE_TRUST_AGENTS} is set and options is not null for all
+ * admins, then it's up to the TrustAgent itself to aggregate the values from all
+ * device admins.
+ * <p>
+ * Consult documentation for the specific TrustAgent to determine legal options
+ * parameters.
+ * @throws SecurityException if {@code admin} is not an active administrator or does not use
+ * {@link DeviceAdminInfo#USES_POLICY_DISABLE_KEYGUARD_FEATURES}
+ */
+ public void setTrustAgentConfiguration(@NonNull ComponentName admin,
+ @NonNull ComponentName target, PersistableBundle configuration) {
+ if (mService != null) {
+ try {
+ mService.setTrustAgentConfiguration(admin, target, configuration, mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Gets configuration for the given trust agent based on aggregating all calls to
+ * {@link #setTrustAgentConfiguration(ComponentName, ComponentName, PersistableBundle)} for
+ * all device admins.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to retrieve the configuration set
+ * on the parent profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. If null,
+ * this function returns a list of configurations for all admins that declare
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS}. If any admin declares
+ * {@link #KEYGUARD_DISABLE_TRUST_AGENTS} but doesn't call
+ * {@link #setTrustAgentConfiguration(ComponentName, ComponentName, PersistableBundle)}
+ * for this {@param agent} or calls it with a null configuration, null is returned.
+ * @param agent Which component to get enabled features for.
+ * @return configuration for the given trust agent.
+ */
+ public @Nullable List<PersistableBundle> getTrustAgentConfiguration(
+ @Nullable ComponentName admin, @NonNull ComponentName agent) {
+ return getTrustAgentConfiguration(admin, agent, myUserId());
+ }
+
+ /** @hide per-user version */
+ public @Nullable List<PersistableBundle> getTrustAgentConfiguration(
+ @Nullable ComponentName admin, @NonNull ComponentName agent, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getTrustAgentConfiguration(admin, agent, userHandle,
+ mParentInstance);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return new ArrayList<PersistableBundle>(); // empty list
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to set whether caller-Id information from the
+ * managed profile will be shown in the parent profile, for incoming calls.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled If true caller-Id information in the managed profile is not displayed.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setCrossProfileCallerIdDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setCrossProfileCallerIdDisabled");
+ if (mService != null) {
+ try {
+ mService.setCrossProfileCallerIdDisabled(admin, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to determine whether or not caller-Id
+ * information has been disabled.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean getCrossProfileCallerIdDisabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("getCrossProfileCallerIdDisabled");
+ if (mService != null) {
+ try {
+ return mService.getCrossProfileCallerIdDisabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether or not caller-Id information has been disabled.
+ *
+ * @param userHandle The user for whom to check the caller-id permission
+ * @hide
+ */
+ public boolean getCrossProfileCallerIdDisabled(UserHandle userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getCrossProfileCallerIdDisabledForUser(userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to set whether contacts search from the
+ * managed profile will be shown in the parent profile, for incoming calls.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled If true contacts search in the managed profile is not displayed.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setCrossProfileContactsSearchDisabled(@NonNull ComponentName admin,
+ boolean disabled) {
+ throwIfParentInstance("setCrossProfileContactsSearchDisabled");
+ if (mService != null) {
+ try {
+ mService.setCrossProfileContactsSearchDisabled(admin, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to determine whether or not contacts search
+ * has been disabled.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean getCrossProfileContactsSearchDisabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("getCrossProfileContactsSearchDisabled");
+ if (mService != null) {
+ try {
+ return mService.getCrossProfileContactsSearchDisabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Determine whether or not contacts search has been disabled.
+ *
+ * @param userHandle The user for whom to check the contacts search permission
+ * @hide
+ */
+ public boolean getCrossProfileContactsSearchDisabled(@NonNull UserHandle userHandle) {
+ if (mService != null) {
+ try {
+ return mService
+ .getCrossProfileContactsSearchDisabledForUser(userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Start Quick Contact on the managed profile for the user, if the policy allows.
+ *
+ * @hide
+ */
+ public void startManagedQuickContact(String actualLookupKey, long actualContactId,
+ boolean isContactIdIgnored, long directoryId, Intent originalIntent) {
+ if (mService != null) {
+ try {
+ mService.startManagedQuickContact(actualLookupKey, actualContactId,
+ isContactIdIgnored, directoryId, originalIntent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Start Quick Contact on the managed profile for the user, if the policy allows.
+ * @hide
+ */
+ public void startManagedQuickContact(String actualLookupKey, long actualContactId,
+ Intent originalIntent) {
+ startManagedQuickContact(actualLookupKey, actualContactId, false, Directory.DEFAULT,
+ originalIntent);
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to set whether bluetooth devices can access
+ * enterprise contacts.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ * <p>
+ * This API works on managed profile only.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled If true, bluetooth devices cannot access enterprise contacts.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setBluetoothContactSharingDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setBluetoothContactSharingDisabled");
+ if (mService != null) {
+ try {
+ mService.setBluetoothContactSharingDisabled(admin, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to determine whether or not Bluetooth devices
+ * cannot access enterprise contacts.
+ * <p>
+ * The calling device admin must be a profile owner. If it is not, a security exception will be
+ * thrown.
+ * <p>
+ * This API works on managed profile only.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean getBluetoothContactSharingDisabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("getBluetoothContactSharingDisabled");
+ if (mService != null) {
+ try {
+ return mService.getBluetoothContactSharingDisabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determine whether or not Bluetooth devices cannot access contacts.
+ * <p>
+ * This API works on managed profile UserHandle only.
+ *
+ * @param userHandle The user for whom to check the caller-id permission
+ * @hide
+ */
+ public boolean getBluetoothContactSharingDisabled(UserHandle userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getBluetoothContactSharingDisabledForUser(userHandle
+ .getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Called by the profile owner of a managed profile so that some intents sent in the managed
+ * profile can also be resolved in the parent, or vice versa. Only activity intents are
+ * supported.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param filter The {@link IntentFilter} the intent has to match to be also resolved in the
+ * other profile
+ * @param flags {@link DevicePolicyManager#FLAG_MANAGED_CAN_ACCESS_PARENT} and
+ * {@link DevicePolicyManager#FLAG_PARENT_CAN_ACCESS_MANAGED} are supported.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void addCrossProfileIntentFilter(@NonNull ComponentName admin, IntentFilter filter, int flags) {
+ throwIfParentInstance("addCrossProfileIntentFilter");
+ if (mService != null) {
+ try {
+ mService.addCrossProfileIntentFilter(admin, filter, flags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to remove the cross-profile intent filters
+ * that go from the managed profile to the parent, or from the parent to the managed profile.
+ * Only removes those that have been set by the profile owner.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void clearCrossProfileIntentFilters(@NonNull ComponentName admin) {
+ throwIfParentInstance("clearCrossProfileIntentFilters");
+ if (mService != null) {
+ try {
+ mService.clearCrossProfileIntentFilters(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile or device owner to set the permitted accessibility services. When set by
+ * a device owner or profile owner the restriction applies to all profiles of the user the
+ * device owner or profile owner is an admin for. By default the user can use any accessiblity
+ * service. When zero or more packages have been added, accessiblity services that are not in
+ * the list and not part of the system can not be enabled by the user.
+ * <p>
+ * Calling with a null value for the list disables the restriction so that all services can be
+ * used, calling with an empty list only allows the builtin system's services.
+ * <p>
+ * System accessibility services are always available to the user the list can't modify this.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageNames List of accessibility service package names.
+ * @return true if setting the restriction succeeded. It fail if there is one or more non-system
+ * accessibility services enabled, that are not in the list.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean setPermittedAccessibilityServices(@NonNull ComponentName admin,
+ List<String> packageNames) {
+ throwIfParentInstance("setPermittedAccessibilityServices");
+ if (mService != null) {
+ try {
+ return mService.setPermittedAccessibilityServices(admin, packageNames);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of permitted accessibility services set by this device or profile owner.
+ * <p>
+ * An empty list means no accessibility services except system services are allowed. Null means
+ * all accessibility services are allowed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return List of accessiblity service package names.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @Nullable List<String> getPermittedAccessibilityServices(@NonNull ComponentName admin) {
+ throwIfParentInstance("getPermittedAccessibilityServices");
+ if (mService != null) {
+ try {
+ return mService.getPermittedAccessibilityServices(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by the system to check if a specific accessibility service is disabled by admin.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName Accessibility service package name that needs to be checked.
+ * @param userHandle user id the admin is running as.
+ * @return true if the accessibility service is permitted, otherwise false.
+ *
+ * @hide
+ */
+ public boolean isAccessibilityServicePermittedByAdmin(@NonNull ComponentName admin,
+ @NonNull String packageName, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.isAccessibilityServicePermittedByAdmin(admin, packageName,
+ userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of accessibility services permitted by the device or profiles
+ * owners of this user.
+ *
+ * <p>Null means all accessibility services are allowed, if a non-null list is returned
+ * it will contain the intersection of the permitted lists for any device or profile
+ * owners that apply to this user. It will also include any system accessibility services.
+ *
+ * @param userId which user to check for.
+ * @return List of accessiblity service package names.
+ * @hide
+ */
+ @SystemApi
+ public @Nullable List<String> getPermittedAccessibilityServices(int userId) {
+ throwIfParentInstance("getPermittedAccessibilityServices");
+ if (mService != null) {
+ try {
+ return mService.getPermittedAccessibilityServicesForUser(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile or device owner to set the permitted input methods services. When set by
+ * a device owner or profile owner the restriction applies to all profiles of the user the
+ * device owner or profile owner is an admin for. By default the user can use any input method.
+ * When zero or more packages have been added, input method that are not in the list and not
+ * part of the system can not be enabled by the user. This method will fail if it is called for
+ * a admin that is not for the foreground user or a profile of the foreground user.
+ * <p>
+ * Calling with a null value for the list disables the restriction so that all input methods can
+ * be used, calling with an empty list disables all but the system's own input methods.
+ * <p>
+ * System input methods are always available to the user this method can't modify this.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageNames List of input method package names.
+ * @return true if setting the restriction succeeded. It will fail if there are one or more
+ * non-system input methods currently enabled that are not in the packageNames list.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean setPermittedInputMethods(
+ @NonNull ComponentName admin, List<String> packageNames) {
+ throwIfParentInstance("setPermittedInputMethods");
+ if (mService != null) {
+ try {
+ return mService.setPermittedInputMethods(admin, packageNames);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Returns the list of permitted input methods set by this device or profile owner.
+ * <p>
+ * An empty list means no input methods except system input methods are allowed. Null means all
+ * input methods are allowed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return List of input method package names.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @Nullable List<String> getPermittedInputMethods(@NonNull ComponentName admin) {
+ throwIfParentInstance("getPermittedInputMethods");
+ if (mService != null) {
+ try {
+ return mService.getPermittedInputMethods(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by the system to check if a specific input method is disabled by admin.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName Input method package name that needs to be checked.
+ * @param userHandle user id the admin is running as.
+ * @return true if the input method is permitted, otherwise false.
+ *
+ * @hide
+ */
+ public boolean isInputMethodPermittedByAdmin(@NonNull ComponentName admin,
+ @NonNull String packageName, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.isInputMethodPermittedByAdmin(admin, packageName, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of input methods permitted by the device or profiles
+ * owners of the current user. (*Not* calling user, due to a limitation in InputMethodManager.)
+ *
+ * <p>Null means all input methods are allowed, if a non-null list is returned
+ * it will contain the intersection of the permitted lists for any device or profile
+ * owners that apply to this user. It will also include any system input methods.
+ *
+ * @return List of input method package names.
+ * @hide
+ */
+ @SystemApi
+ public @Nullable List<String> getPermittedInputMethodsForCurrentUser() {
+ throwIfParentInstance("getPermittedInputMethodsForCurrentUser");
+ if (mService != null) {
+ try {
+ return mService.getPermittedInputMethodsForCurrentUser();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to set the packages that are allowed to use
+ * a {@link android.service.notification.NotificationListenerService} in the primary user to
+ * see notifications from the managed profile. By default all packages are permitted by this
+ * policy. When zero or more packages have been added, notification listeners installed on the
+ * primary user that are not in the list and are not part of the system won't receive events
+ * for managed profile notifications.
+ * <p>
+ * Calling with a {@code null} value for the list disables the restriction so that all
+ * notification listener services be used. Calling with an empty list disables all but the
+ * system's own notification listeners. System notification listener services are always
+ * available to the user.
+ * <p>
+ * If a device or profile owner want to stop notification listeners in their user from seeing
+ * that user's notifications they should prevent that service from running instead (e.g. via
+ * {@link #setApplicationHidden(ComponentName, String, boolean)})
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageList List of package names to whitelist
+ * @return true if setting the restriction succeeded. It will fail if called outside a managed
+ * profile
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ *
+ * @see android.service.notification.NotificationListenerService
+ */
+ public boolean setPermittedCrossProfileNotificationListeners(
+ @NonNull ComponentName admin, @Nullable List<String> packageList) {
+ throwIfParentInstance("setPermittedCrossProfileNotificationListeners");
+ if (mService != null) {
+ try {
+ return mService.setPermittedCrossProfileNotificationListeners(admin, packageList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of packages installed on the primary user that allowed to use a
+ * {@link android.service.notification.NotificationListenerService} to receive
+ * notifications from this managed profile, as set by the profile owner.
+ * <p>
+ * An empty list means no notification listener services except system ones are allowed.
+ * A {@code null} return value indicates that all notification listeners are allowed.
+ */
+ public @Nullable List<String> getPermittedCrossProfileNotificationListeners(
+ @NonNull ComponentName admin) {
+ throwIfParentInstance("getPermittedCrossProfileNotificationListeners");
+ if (mService != null) {
+ try {
+ return mService.getPermittedCrossProfileNotificationListeners(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if {@code NotificationListenerServices} from the given package are allowed to
+ * receive events for notifications from the given user id. Can only be called by the system uid
+ *
+ * @see #setPermittedCrossProfileNotificationListeners(ComponentName, List)
+ *
+ * @hide
+ */
+ public boolean isNotificationListenerServicePermitted(
+ @NonNull String packageName, @UserIdInt int userId) {
+ if (mService != null) {
+ try {
+ return mService.isNotificationListenerServicePermitted(packageName, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get the list of apps to keep around as APKs even if no user has currently installed it. This
+ * function can be called by a device owner or by a delegate given the
+ * {@link #DELEGATION_KEEP_UNINSTALLED_PACKAGES} scope via {@link #setDelegatedScopes}.
+ * <p>
+ * Please note that packages returned in this method are not automatically pre-cached.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a keep uninstalled packages delegate.
+ * @return List of package names to keep cached.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES
+ * @hide
+ */
+ public @Nullable List<String> getKeepUninstalledPackages(@Nullable ComponentName admin) {
+ throwIfParentInstance("getKeepUninstalledPackages");
+ if (mService != null) {
+ try {
+ return mService.getKeepUninstalledPackages(admin, mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set a list of apps to keep around as APKs even if no user has currently installed it. This
+ * function can be called by a device owner or by a delegate given the
+ * {@link #DELEGATION_KEEP_UNINSTALLED_PACKAGES} scope via {@link #setDelegatedScopes}.
+ *
+ * <p>Please note that setting this policy does not imply that specified apps will be
+ * automatically pre-cached.</p>
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a keep uninstalled packages delegate.
+ * @param packageNames List of package names to keep cached.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES
+ * @hide
+ */
+ public void setKeepUninstalledPackages(@Nullable ComponentName admin,
+ @NonNull List<String> packageNames) {
+ throwIfParentInstance("setKeepUninstalledPackages");
+ if (mService != null) {
+ try {
+ mService.setKeepUninstalledPackages(admin, mContext.getPackageName(), packageNames);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a device owner to create a user with the specified name. The UserHandle returned
+ * by this method should not be persisted as user handles are recycled as users are removed and
+ * created. If you need to persist an identifier for this user, use
+ * {@link UserManager#getSerialNumberForUser}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param name the user's name
+ * @see UserHandle
+ * @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
+ * user could not be created.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#M}
+ * @removed From {@link android.os.Build.VERSION_CODES#N}
+ */
+ @Deprecated
+ public @Nullable UserHandle createUser(@NonNull ComponentName admin, String name) {
+ return null;
+ }
+
+ /**
+ * Called by a device owner to create a user with the specified name. The UserHandle returned
+ * by this method should not be persisted as user handles are recycled as users are removed and
+ * created. If you need to persist an identifier for this user, use
+ * {@link UserManager#getSerialNumberForUser}. The new user will be started in the background
+ * immediately.
+ *
+ * <p> profileOwnerComponent is the {@link DeviceAdminReceiver} to be the profile owner as well
+ * as registered as an active admin on the new user. The profile owner package will be
+ * installed on the new user if it already is installed on the device.
+ *
+ * <p>If the optionalInitializeData is not null, then the extras will be passed to the
+ * profileOwnerComponent when onEnable is called.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param name the user's name
+ * @param ownerName the human readable name of the organisation associated with this DPM.
+ * @param profileOwnerComponent The {@link DeviceAdminReceiver} that will be an active admin on
+ * the user.
+ * @param adminExtras Extras that will be passed to onEnable of the admin receiver
+ * on the new user.
+ * @see UserHandle
+ * @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
+ * user could not be created.
+ *
+ * @deprecated From {@link android.os.Build.VERSION_CODES#M}
+ * @removed From {@link android.os.Build.VERSION_CODES#N}
+ */
+ @Deprecated
+ public @Nullable UserHandle createAndInitializeUser(@NonNull ComponentName admin, String name,
+ String ownerName, @NonNull ComponentName profileOwnerComponent, Bundle adminExtras) {
+ return null;
+ }
+
+ /**
+ * Flag used by {@link #createAndManageUser} to skip setup wizard after creating a new user.
+ */
+ public static final int SKIP_SETUP_WIZARD = 0x0001;
+
+ /**
+ * Flag used by {@link #createAndManageUser} to specify that the user should be created
+ * ephemeral.
+ * @hide
+ */
+ public static final int MAKE_USER_EPHEMERAL = 0x0002;
+
+ /**
+ * Flag used by {@link #createAndManageUser} to specify that the user should be created as a
+ * demo user.
+ * @hide
+ */
+ public static final int MAKE_USER_DEMO = 0x0004;
+
+ /**
+ * Flag used by {@link #createAndManageUser} to specificy that the newly created user should be
+ * started in the background as part of the user creation.
+ */
+ // TODO: Investigate solutions for the case where reboot happens before setup is completed.
+ public static final int START_USER_IN_BACKGROUND = 0x0008;
+
+ /**
+ * @hide
+ */
+ @IntDef(
+ flag = true,
+ prefix = {"SKIP_", "MAKE_USER_", "START_"},
+ value = {SKIP_SETUP_WIZARD, MAKE_USER_EPHEMERAL, MAKE_USER_DEMO,
+ START_USER_IN_BACKGROUND}
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CreateAndManageUserFlags {}
+
+
+ /**
+ * Called by a device owner to create a user with the specified name and a given component of
+ * the calling package as profile owner. The UserHandle returned by this method should not be
+ * persisted as user handles are recycled as users are removed and created. If you need to
+ * persist an identifier for this user, use {@link UserManager#getSerialNumberForUser}. The new
+ * user will not be started in the background.
+ * <p>
+ * admin is the {@link DeviceAdminReceiver} which is the device owner. profileOwner is also a
+ * DeviceAdminReceiver in the same package as admin, and will become the profile owner and will
+ * be registered as an active admin on the new user. The profile owner package will be installed
+ * on the new user.
+ * <p>
+ * If the adminExtras are not null, they will be stored on the device until the user is started
+ * for the first time. Then the extras will be passed to the admin when onEnable is called.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param name The user's name.
+ * @param profileOwner Which {@link DeviceAdminReceiver} will be profile owner. Has to be in the
+ * same package as admin, otherwise no user is created and an
+ * IllegalArgumentException is thrown.
+ * @param adminExtras Extras that will be passed to onEnable of the admin receiver on the new
+ * user.
+ * @param flags {@link #SKIP_SETUP_WIZARD} is supported.
+ * @see UserHandle
+ * @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
+ * user could not be created.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public @Nullable UserHandle createAndManageUser(@NonNull ComponentName admin,
+ @NonNull String name,
+ @NonNull ComponentName profileOwner, @Nullable PersistableBundle adminExtras,
+ @CreateAndManageUserFlags int flags) {
+ throwIfParentInstance("createAndManageUser");
+ try {
+ return mService.createAndManageUser(admin, name, profileOwner, adminExtras, flags);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to remove a user and all associated data. The primary user can not
+ * be removed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle the user to remove.
+ * @return {@code true} if the user was removed, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public boolean removeUser(@NonNull ComponentName admin, UserHandle userHandle) {
+ throwIfParentInstance("removeUser");
+ try {
+ return mService.removeUser(admin, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to switch the specified user to the foreground.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle the user to switch to; null will switch to primary.
+ * @return {@code true} if the switch was successful, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see Intent#ACTION_USER_FOREGROUND
+ */
+ public boolean switchUser(@NonNull ComponentName admin, @Nullable UserHandle userHandle) {
+ throwIfParentInstance("switchUser");
+ try {
+ return mService.switchUser(admin, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the application restrictions for a given target application running in the calling
+ * user.
+ * <p>
+ * The caller must be a profile or device owner on that user, or the package allowed to manage
+ * application restrictions via {@link #setDelegatedScopes} with the
+ * {@link #DELEGATION_APP_RESTRICTIONS} scope; otherwise a security exception will be thrown.
+ *
+ * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if called by the application restrictions managing package.
+ * @param packageName The name of the package to fetch restricted settings of.
+ * @return {@link Bundle} of settings corresponding to what was set last time
+ * {@link DevicePolicyManager#setApplicationRestrictions} was called, or an empty
+ * {@link Bundle} if no restrictions have been set.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_APP_RESTRICTIONS
+ */
+ @WorkerThread
+ public @NonNull Bundle getApplicationRestrictions(
+ @Nullable ComponentName admin, String packageName) {
+ throwIfParentInstance("getApplicationRestrictions");
+ if (mService != null) {
+ try {
+ return mService.getApplicationRestrictions(admin, mContext.getPackageName(),
+ packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a profile or device owner to set a user restriction specified by the key.
+ * <p>
+ * The calling device admin must be a profile or device owner; if it is not, a security
+ * exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param key The key of the restriction. See the constants in {@link android.os.UserManager}
+ * for the list of keys.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void addUserRestriction(@NonNull ComponentName admin, String key) {
+ throwIfParentInstance("addUserRestriction");
+ if (mService != null) {
+ try {
+ mService.setUserRestriction(admin, key, true);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile or device owner to clear a user restriction specified by the key.
+ * <p>
+ * The calling device admin must be a profile or device owner; if it is not, a security
+ * exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param key The key of the restriction. See the constants in {@link android.os.UserManager}
+ * for the list of keys.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void clearUserRestriction(@NonNull ComponentName admin, String key) {
+ throwIfParentInstance("clearUserRestriction");
+ if (mService != null) {
+ try {
+ mService.setUserRestriction(admin, key, false);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile or device owner to get user restrictions set with
+ * {@link #addUserRestriction(ComponentName, String)}.
+ * <p>
+ * The target user may have more restrictions set by the system or other device owner / profile
+ * owner. To get all the user restrictions currently set, use
+ * {@link UserManager#getUserRestrictions()}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @NonNull Bundle getUserRestrictions(@NonNull ComponentName admin) {
+ throwIfParentInstance("getUserRestrictions");
+ Bundle ret = null;
+ if (mService != null) {
+ try {
+ ret = mService.getUserRestrictions(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return ret == null ? new Bundle() : ret;
+ }
+
+ /**
+ * Called by any app to display a support dialog when a feature was disabled by an admin.
+ * This returns an intent that can be used with {@link Context#startActivity(Intent)} to
+ * display the dialog. It will tell the user that the feature indicated by {@code restriction}
+ * was disabled by an admin, and include a link for more information. The default content of
+ * the dialog can be changed by the restricting admin via
+ * {@link #setShortSupportMessage(ComponentName, CharSequence)}. If the restriction is not
+ * set (i.e. the feature is available), then the return value will be {@code null}.
+ * @param restriction Indicates for which feature the dialog should be displayed. Can be a
+ * user restriction from {@link UserManager}, e.g.
+ * {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants
+ * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * @return Intent An intent to be used to start the dialog-activity if the restriction is
+ * set by an admin, or null if the restriction does not exist or no admin set it.
+ */
+ public Intent createAdminSupportIntent(@NonNull String restriction) {
+ throwIfParentInstance("createAdminSupportIntent");
+ if (mService != null) {
+ try {
+ return mService.createAdminSupportIntent(restriction);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Hide or unhide packages. When a package is hidden it is unavailable for use, but the data and
+ * actual package file remain. This function can be called by a device owner, profile owner, or
+ * by a delegate given the {@link #DELEGATION_PACKAGE_ACCESS} scope via
+ * {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a package access delegate.
+ * @param packageName The name of the package to hide or unhide.
+ * @param hidden {@code true} if the package should be hidden, {@code false} if it should be
+ * unhidden.
+ * @return boolean Whether the hidden setting of the package was successfully updated.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public boolean setApplicationHidden(@NonNull ComponentName admin, String packageName,
+ boolean hidden) {
+ throwIfParentInstance("setApplicationHidden");
+ if (mService != null) {
+ try {
+ return mService.setApplicationHidden(admin, mContext.getPackageName(), packageName,
+ hidden);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine if a package is hidden. This function can be called by a device owner, profile
+ * owner, or by a delegate given the {@link #DELEGATION_PACKAGE_ACCESS} scope via
+ * {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a package access delegate.
+ * @param packageName The name of the package to retrieve the hidden status of.
+ * @return boolean {@code true} if the package is hidden, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public boolean isApplicationHidden(@NonNull ComponentName admin, String packageName) {
+ throwIfParentInstance("isApplicationHidden");
+ if (mService != null) {
+ try {
+ return mService.isApplicationHidden(admin, mContext.getPackageName(), packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Re-enable a system app that was disabled by default when the user was initialized. This
+ * function can be called by a device owner, profile owner, or by a delegate given the
+ * {@link #DELEGATION_ENABLE_SYSTEM_APP} scope via {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is an enable system app delegate.
+ * @param packageName The package to be re-enabled in the calling profile.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public void enableSystemApp(@NonNull ComponentName admin, String packageName) {
+ throwIfParentInstance("enableSystemApp");
+ if (mService != null) {
+ try {
+ mService.enableSystemApp(admin, mContext.getPackageName(), packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Re-enable system apps by intent that were disabled by default when the user was initialized.
+ * This function can be called by a device owner, profile owner, or by a delegate given the
+ * {@link #DELEGATION_ENABLE_SYSTEM_APP} scope via {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is an enable system app delegate.
+ * @param intent An intent matching the app(s) to be installed. All apps that resolve for this
+ * intent will be re-enabled in the calling profile.
+ * @return int The number of activities that matched the intent and were installed.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public int enableSystemApp(@NonNull ComponentName admin, Intent intent) {
+ throwIfParentInstance("enableSystemApp");
+ if (mService != null) {
+ try {
+ return mService.enableSystemAppWithIntent(admin, mContext.getPackageName(), intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by a device owner or profile owner to disable account management for a specific type
+ * of account.
+ * <p>
+ * The calling device admin must be a device owner or profile owner. If it is not, a security
+ * exception will be thrown.
+ * <p>
+ * When account management is disabled for an account type, adding or removing an account of
+ * that type will not be possible.
+ * <p>
+ * From {@link android.os.Build.VERSION_CODES#N} the profile or device owner can still use
+ * {@link android.accounts.AccountManager} APIs to add or remove accounts when account
+ * management for a specific type is disabled.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param accountType For which account management is disabled or enabled.
+ * @param disabled The boolean indicating that account management will be disabled (true) or
+ * enabled (false).
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setAccountManagementDisabled(@NonNull ComponentName admin, String accountType,
+ boolean disabled) {
+ throwIfParentInstance("setAccountManagementDisabled");
+ if (mService != null) {
+ try {
+ mService.setAccountManagementDisabled(admin, accountType, disabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Gets the array of accounts for which account management is disabled by the profile owner.
+ *
+ * <p> Account management can be disabled/enabled by calling
+ * {@link #setAccountManagementDisabled}.
+ *
+ * @return a list of account types for which account management has been disabled.
+ *
+ * @see #setAccountManagementDisabled
+ */
+ public @Nullable String[] getAccountTypesWithManagementDisabled() {
+ throwIfParentInstance("getAccountTypesWithManagementDisabled");
+ return getAccountTypesWithManagementDisabledAsUser(myUserId());
+ }
+
+ /**
+ * @see #getAccountTypesWithManagementDisabled()
+ * @hide
+ */
+ public @Nullable String[] getAccountTypesWithManagementDisabledAsUser(int userId) {
+ if (mService != null) {
+ try {
+ return mService.getAccountTypesWithManagementDisabledAsUser(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets which packages may enter lock task mode.
+ * <p>
+ * Any packages that share uid with an allowed package will also be allowed to activate lock
+ * task. From {@link android.os.Build.VERSION_CODES#M} removing packages from the lock task
+ * package list results in locked tasks belonging to those packages to be finished.
+ * <p>
+ * This function can only be called by the device owner or by a profile owner of a user/profile
+ * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any packages
+ * set via this method will be cleared if the user becomes unaffiliated.
+ *
+ * @param packages The list of packages allowed to enter lock task mode
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
+ * an affiliated user or profile.
+ * @see Activity#startLockTask()
+ * @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String)
+ * @see DeviceAdminReceiver#onLockTaskModeExiting(Context, Intent)
+ * @see UserManager#DISALLOW_CREATE_WINDOWS
+ */
+ public void setLockTaskPackages(@NonNull ComponentName admin, @NonNull String[] packages)
+ throws SecurityException {
+ throwIfParentInstance("setLockTaskPackages");
+ if (mService != null) {
+ try {
+ mService.setLockTaskPackages(admin, packages);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the list of packages allowed to start the lock task mode.
+ *
+ * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
+ * an affiliated user or profile.
+ * @see #setLockTaskPackages
+ */
+ public @NonNull String[] getLockTaskPackages(@NonNull ComponentName admin) {
+ throwIfParentInstance("getLockTaskPackages");
+ if (mService != null) {
+ try {
+ return mService.getLockTaskPackages(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return new String[0];
+ }
+
+ /**
+ * This function lets the caller know whether the given component is allowed to start the
+ * lock task mode.
+ * @param pkg The package to check
+ */
+ public boolean isLockTaskPermitted(String pkg) {
+ throwIfParentInstance("isLockTaskPermitted");
+ if (mService != null) {
+ try {
+ return mService.isLockTaskPermitted(pkg);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device owners to update {@link android.provider.Settings.Global} settings.
+ * Validation that the value of the setting is in the correct form for the setting type should
+ * be performed by the caller.
+ * <p>
+ * The settings that can be updated with this method are:
+ * <ul>
+ * <li>{@link android.provider.Settings.Global#ADB_ENABLED}</li>
+ * <li>{@link android.provider.Settings.Global#AUTO_TIME}</li>
+ * <li>{@link android.provider.Settings.Global#AUTO_TIME_ZONE}</li>
+ * <li>{@link android.provider.Settings.Global#DATA_ROAMING}</li>
+ * <li>{@link android.provider.Settings.Global#USB_MASS_STORAGE_ENABLED}</li>
+ * <li>{@link android.provider.Settings.Global#WIFI_SLEEP_POLICY}</li>
+ * <li>{@link android.provider.Settings.Global#STAY_ON_WHILE_PLUGGED_IN} This setting is only
+ * available from {@link android.os.Build.VERSION_CODES#M} onwards and can only be set if
+ * {@link #setMaximumTimeToLock} is not used to set a timeout.</li>
+ * <li>{@link android.provider.Settings.Global#WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN}</li> This
+ * setting is only available from {@link android.os.Build.VERSION_CODES#M} onwards.</li>
+ * </ul>
+ * <p>
+ * Changing the following settings has no effect as of {@link android.os.Build.VERSION_CODES#M}:
+ * <ul>
+ * <li>{@link android.provider.Settings.Global#BLUETOOTH_ON}. Use
+ * {@link android.bluetooth.BluetoothAdapter#enable()} and
+ * {@link android.bluetooth.BluetoothAdapter#disable()} instead.</li>
+ * <li>{@link android.provider.Settings.Global#DEVELOPMENT_SETTINGS_ENABLED}</li>
+ * <li>{@link android.provider.Settings.Global#MODE_RINGER}. Use
+ * {@link android.media.AudioManager#setRingerMode(int)} instead.</li>
+ * <li>{@link android.provider.Settings.Global#NETWORK_PREFERENCE}</li>
+ * <li>{@link android.provider.Settings.Global#WIFI_ON}. Use
+ * {@link android.net.wifi.WifiManager#setWifiEnabled(boolean)} instead.</li>
+ * </ul>
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param setting The name of the setting to update.
+ * @param value The value to update the setting to.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setGlobalSetting(@NonNull ComponentName admin, String setting, String value) {
+ throwIfParentInstance("setGlobalSetting");
+ if (mService != null) {
+ try {
+ mService.setGlobalSetting(admin, setting, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by profile or device owners to update {@link android.provider.Settings.Secure}
+ * settings. Validation that the value of the setting is in the correct form for the setting
+ * type should be performed by the caller.
+ * <p>
+ * The settings that can be updated by a profile or device owner with this method are:
+ * <ul>
+ * <li>{@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD}</li>
+ * <li>{@link android.provider.Settings.Secure#SKIP_FIRST_USE_HINTS}</li>
+ * </ul>
+ * <p>
+ * A device owner can additionally update the following settings:
+ * <ul>
+ * <li>{@link android.provider.Settings.Secure#LOCATION_MODE}</li>
+ * </ul>
+ *
+ * <strong>Note: Starting from Android O, apps should no longer call this method with the
+ * setting {@link android.provider.Settings.Secure#INSTALL_NON_MARKET_APPS}, which is
+ * deprecated. Instead, device owners or profile owners should use the restriction
+ * {@link UserManager#DISALLOW_INSTALL_UNKNOWN_SOURCES}.
+ * If any app targeting {@link android.os.Build.VERSION_CODES#O} or higher calls this method
+ * with {@link android.provider.Settings.Secure#INSTALL_NON_MARKET_APPS},
+ * an {@link UnsupportedOperationException} is thrown.
+ * </strong>
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param setting The name of the setting to update.
+ * @param value The value to update the setting to.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setSecureSetting(@NonNull ComponentName admin, String setting, String value) {
+ throwIfParentInstance("setSecureSetting");
+ if (mService != null) {
+ try {
+ mService.setSecureSetting(admin, setting, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Designates a specific service component as the provider for making permission requests of a
+ * local or remote administrator of the user.
+ * <p/>
+ * Only a profile owner can designate the restrictions provider.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param provider The component name of the service that implements
+ * {@link RestrictionsReceiver}. If this param is null, it removes the restrictions
+ * provider previously assigned.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setRestrictionsProvider(@NonNull ComponentName admin,
+ @Nullable ComponentName provider) {
+ throwIfParentInstance("setRestrictionsProvider");
+ if (mService != null) {
+ try {
+ mService.setRestrictionsProvider(admin, provider);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by profile or device owners to set the master volume mute on or off.
+ * This has no effect when set on a managed profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param on {@code true} to mute master volume, {@code false} to turn mute off.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setMasterVolumeMuted(@NonNull ComponentName admin, boolean on) {
+ throwIfParentInstance("setMasterVolumeMuted");
+ if (mService != null) {
+ try {
+ mService.setMasterVolumeMuted(admin, on);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by profile or device owners to check whether the master volume mute is on or off.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return {@code true} if master volume is muted, {@code false} if it's not.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean isMasterVolumeMuted(@NonNull ComponentName admin) {
+ throwIfParentInstance("isMasterVolumeMuted");
+ if (mService != null) {
+ try {
+ return mService.isMasterVolumeMuted(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Change whether a user can uninstall a package. This function can be called by a device owner,
+ * profile owner, or by a delegate given the {@link #DELEGATION_BLOCK_UNINSTALL} scope via
+ * {@link #setDelegatedScopes}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if the caller is a block uninstall delegate.
+ * @param packageName package to change.
+ * @param uninstallBlocked true if the user shouldn't be able to uninstall the package.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_BLOCK_UNINSTALL
+ */
+ public void setUninstallBlocked(@Nullable ComponentName admin, String packageName,
+ boolean uninstallBlocked) {
+ throwIfParentInstance("setUninstallBlocked");
+ if (mService != null) {
+ try {
+ mService.setUninstallBlocked(admin, mContext.getPackageName(), packageName,
+ uninstallBlocked);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Check whether the user has been blocked by device policy from uninstalling a package.
+ * Requires the caller to be the profile owner if checking a specific admin's policy.
+ * <p>
+ * <strong>Note:</strong> Starting from {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}, the
+ * behavior of this API is changed such that passing {@code null} as the {@code admin} parameter
+ * will return if any admin has blocked the uninstallation. Before L MR1, passing {@code null}
+ * will cause a NullPointerException to be raised.
+ *
+ * @param admin The name of the admin component whose blocking policy will be checked, or
+ * {@code null} to check whether any admin has blocked the uninstallation.
+ * @param packageName package to check.
+ * @return true if uninstallation is blocked.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public boolean isUninstallBlocked(@Nullable ComponentName admin, String packageName) {
+ throwIfParentInstance("isUninstallBlocked");
+ if (mService != null) {
+ try {
+ return mService.isUninstallBlocked(admin, packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by the profile owner of a managed profile to enable widget providers from a given
+ * package to be available in the parent profile. As a result the user will be able to add
+ * widgets from the white-listed package running under the profile to a widget host which runs
+ * under the parent profile, for example the home screen. Note that a package may have zero or
+ * more provider components, where each component provides a different widget type.
+ * <p>
+ * <strong>Note:</strong> By default no widget provider package is white-listed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The package from which widget providers are white-listed.
+ * @return Whether the package was added.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ * @see #removeCrossProfileWidgetProvider(android.content.ComponentName, String)
+ * @see #getCrossProfileWidgetProviders(android.content.ComponentName)
+ */
+ public boolean addCrossProfileWidgetProvider(@NonNull ComponentName admin, String packageName) {
+ throwIfParentInstance("addCrossProfileWidgetProvider");
+ if (mService != null) {
+ try {
+ return mService.addCrossProfileWidgetProvider(admin, packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by the profile owner of a managed profile to disable widget providers from a given
+ * package to be available in the parent profile. For this method to take effect the package
+ * should have been added via
+ * {@link #addCrossProfileWidgetProvider( android.content.ComponentName, String)}.
+ * <p>
+ * <strong>Note:</strong> By default no widget provider package is white-listed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The package from which widget providers are no longer white-listed.
+ * @return Whether the package was removed.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ * @see #addCrossProfileWidgetProvider(android.content.ComponentName, String)
+ * @see #getCrossProfileWidgetProviders(android.content.ComponentName)
+ */
+ public boolean removeCrossProfileWidgetProvider(
+ @NonNull ComponentName admin, String packageName) {
+ throwIfParentInstance("removeCrossProfileWidgetProvider");
+ if (mService != null) {
+ try {
+ return mService.removeCrossProfileWidgetProvider(admin, packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by the profile owner of a managed profile to query providers from which packages are
+ * available in the parent profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The white-listed package list.
+ * @see #addCrossProfileWidgetProvider(android.content.ComponentName, String)
+ * @see #removeCrossProfileWidgetProvider(android.content.ComponentName, String)
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public @NonNull List<String> getCrossProfileWidgetProviders(@NonNull ComponentName admin) {
+ throwIfParentInstance("getCrossProfileWidgetProviders");
+ if (mService != null) {
+ try {
+ List<String> providers = mService.getCrossProfileWidgetProviders(admin);
+ if (providers != null) {
+ return providers;
+ }
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Called by profile or device owners to set the user's photo.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param icon the bitmap to set as the photo.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setUserIcon(@NonNull ComponentName admin, Bitmap icon) {
+ throwIfParentInstance("setUserIcon");
+ try {
+ mService.setUserIcon(admin, icon);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owners to set a local system update policy. When a new policy is set,
+ * {@link #ACTION_SYSTEM_UPDATE_POLICY_CHANGED} is broadcasted.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. All
+ * components in the device owner package can set system update policies and the most
+ * recent policy takes effect.
+ * @param policy the new policy, or {@code null} to clear the current policy.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see SystemUpdatePolicy
+ */
+ public void setSystemUpdatePolicy(@NonNull ComponentName admin, SystemUpdatePolicy policy) {
+ throwIfParentInstance("setSystemUpdatePolicy");
+ if (mService != null) {
+ try {
+ mService.setSystemUpdatePolicy(admin, policy);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Retrieve a local system update policy set previously by {@link #setSystemUpdatePolicy}.
+ *
+ * @return The current policy object, or {@code null} if no policy is set.
+ */
+ public @Nullable SystemUpdatePolicy getSystemUpdatePolicy() {
+ throwIfParentInstance("getSystemUpdatePolicy");
+ if (mService != null) {
+ try {
+ return mService.getSystemUpdatePolicy();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a device owner to disable the keyguard altogether.
+ * <p>
+ * Setting the keyguard to disabled has the same effect as choosing "None" as the screen lock
+ * type. However, this call has no effect if a password, pin or pattern is currently set. If a
+ * password, pin or pattern is set after the keyguard was disabled, the keyguard stops being
+ * disabled.
+ *
+ * <p>
+ * As of {@link android.os.Build.VERSION_CODES#P}, this call also dismisses the
+ * keyguard if it is currently shown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled {@code true} disables the keyguard, {@code false} reenables it.
+ * @return {@code false} if attempting to disable the keyguard while a lock password was in
+ * place. {@code true} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public boolean setKeyguardDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setKeyguardDisabled");
+ try {
+ return mService.setKeyguardDisabled(admin, disabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to disable the status bar. Disabling the status bar blocks
+ * notifications, quick settings and other screen overlays that allow escaping from a single use
+ * device.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param disabled {@code true} disables the status bar, {@code false} reenables it.
+ * @return {@code false} if attempting to disable the status bar failed. {@code true} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public boolean setStatusBarDisabled(@NonNull ComponentName admin, boolean disabled) {
+ throwIfParentInstance("setStatusBarDisabled");
+ try {
+ return mService.setStatusBarDisabled(admin, disabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system update service to notify device and profile owners of pending system
+ * updates.
+ *
+ * This method should only be used when it is unknown whether the pending system
+ * update is a security patch. Otherwise, use
+ * {@link #notifyPendingSystemUpdate(long, boolean)}.
+ *
+ * @param updateReceivedTime The time as given by {@link System#currentTimeMillis()}
+ * indicating when the current pending update was first available. {@code -1} if no
+ * update is available.
+ * @see #notifyPendingSystemUpdate(long, boolean)
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.NOTIFY_PENDING_SYSTEM_UPDATE)
+ public void notifyPendingSystemUpdate(long updateReceivedTime) {
+ throwIfParentInstance("notifyPendingSystemUpdate");
+ if (mService != null) {
+ try {
+ mService.notifyPendingSystemUpdate(SystemUpdateInfo.of(updateReceivedTime));
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by the system update service to notify device and profile owners of pending system
+ * updates.
+ *
+ * This method should be used instead of {@link #notifyPendingSystemUpdate(long)}
+ * when it is known whether the pending system update is a security patch.
+ *
+ * @param updateReceivedTime The time as given by {@link System#currentTimeMillis()}
+ * indicating when the current pending update was first available. {@code -1} if no
+ * update is available.
+ * @param isSecurityPatch {@code true} if this system update is purely a security patch;
+ * {@code false} if not.
+ * @see #notifyPendingSystemUpdate(long)
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.NOTIFY_PENDING_SYSTEM_UPDATE)
+ public void notifyPendingSystemUpdate(long updateReceivedTime, boolean isSecurityPatch) {
+ throwIfParentInstance("notifyPendingSystemUpdate");
+ if (mService != null) {
+ try {
+ mService.notifyPendingSystemUpdate(SystemUpdateInfo.of(updateReceivedTime,
+ isSecurityPatch));
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by device or profile owners to get information about a pending system update.
+ *
+ * @param admin Which profile or device owner this request is associated with.
+ * @return Information about a pending system update or {@code null} if no update pending.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see DeviceAdminReceiver#onSystemUpdatePending(Context, Intent, long)
+ */
+ public @Nullable SystemUpdateInfo getPendingSystemUpdate(@NonNull ComponentName admin) {
+ throwIfParentInstance("getPendingSystemUpdate");
+ try {
+ return mService.getPendingSystemUpdate(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the default response for future runtime permission requests by applications. This
+ * function can be called by a device owner, profile owner, or by a delegate given the
+ * {@link #DELEGATION_PERMISSION_GRANT} scope via {@link #setDelegatedScopes}.
+ * The policy can allow for normal operation which prompts the user to grant a permission, or
+ * can allow automatic granting or denying of runtime permission requests by an application.
+ * This also applies to new permissions declared by app updates. When a permission is denied or
+ * granted this way, the effect is equivalent to setting the permission * grant state via
+ * {@link #setPermissionGrantState}.
+ * <p/>
+ * As this policy only acts on runtime permission requests, it only applies to applications
+ * built with a {@code targetSdkVersion} of {@link android.os.Build.VERSION_CODES#M} or later.
+ *
+ * @param admin Which profile or device owner this request is associated with.
+ * @param policy One of the policy constants {@link #PERMISSION_POLICY_PROMPT},
+ * {@link #PERMISSION_POLICY_AUTO_GRANT} and {@link #PERMISSION_POLICY_AUTO_DENY}.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setPermissionGrantState
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PERMISSION_GRANT
+ */
+ public void setPermissionPolicy(@NonNull ComponentName admin, int policy) {
+ throwIfParentInstance("setPermissionPolicy");
+ try {
+ mService.setPermissionPolicy(admin, mContext.getPackageName(), policy);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current runtime permission policy set by the device or profile owner. The
+ * default is {@link #PERMISSION_POLICY_PROMPT}.
+ *
+ * @param admin Which profile or device owner this request is associated with.
+ * @return the current policy for future permission requests.
+ */
+ public int getPermissionPolicy(ComponentName admin) {
+ throwIfParentInstance("getPermissionPolicy");
+ try {
+ return mService.getPermissionPolicy(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the grant state of a runtime permission for a specific application. The state can be
+ * {@link #PERMISSION_GRANT_STATE_DEFAULT default} in which a user can manage it through the UI,
+ * {@link #PERMISSION_GRANT_STATE_DENIED denied}, in which the permission is denied and the user
+ * cannot manage it through the UI, and {@link #PERMISSION_GRANT_STATE_GRANTED granted} in which
+ * the permission is granted and the user cannot manage it through the UI. This might affect all
+ * permissions in a group that the runtime permission belongs to. This method can only be called
+ * by a profile owner, device owner, or a delegate given the
+ * {@link #DELEGATION_PERMISSION_GRANT} scope via {@link #setDelegatedScopes}.
+ * <p/>
+ * Setting the grant state to {@link #PERMISSION_GRANT_STATE_DEFAULT default} does not revoke
+ * the permission. It retains the previous grant, if any.
+ * <p/>
+ * Permissions can be granted or revoked only for applications built with a
+ * {@code targetSdkVersion} of {@link android.os.Build.VERSION_CODES#M} or later.
+ *
+ * @param admin Which profile or device owner this request is associated with.
+ * @param packageName The application to grant or revoke a permission to.
+ * @param permission The permission to grant or revoke.
+ * @param grantState The permission grant state which is one of
+ * {@link #PERMISSION_GRANT_STATE_DENIED}, {@link #PERMISSION_GRANT_STATE_DEFAULT},
+ * {@link #PERMISSION_GRANT_STATE_GRANTED},
+ * @return whether the permission was successfully granted or revoked.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #PERMISSION_GRANT_STATE_DENIED
+ * @see #PERMISSION_GRANT_STATE_DEFAULT
+ * @see #PERMISSION_GRANT_STATE_GRANTED
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PERMISSION_GRANT
+ */
+ public boolean setPermissionGrantState(@NonNull ComponentName admin, String packageName,
+ String permission, int grantState) {
+ throwIfParentInstance("setPermissionGrantState");
+ try {
+ return mService.setPermissionGrantState(admin, mContext.getPackageName(), packageName,
+ permission, grantState);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current grant state of a runtime permission for a specific application. This
+ * function can be called by a device owner, profile owner, or by a delegate given the
+ * {@link #DELEGATION_PERMISSION_GRANT} scope via {@link #setDelegatedScopes}.
+ *
+ * @param admin Which profile or device owner this request is associated with, or {@code null}
+ * if the caller is a permission grant delegate.
+ * @param packageName The application to check the grant state for.
+ * @param permission The permission to check for.
+ * @return the current grant state specified by device policy. If the profile or device owner
+ * has not set a grant state, the return value is
+ * {@link #PERMISSION_GRANT_STATE_DEFAULT}. This does not indicate whether or not the
+ * permission is currently granted for the package.
+ * <p/>
+ * If a grant state was set by the profile or device owner, then the return value will
+ * be one of {@link #PERMISSION_GRANT_STATE_DENIED} or
+ * {@link #PERMISSION_GRANT_STATE_GRANTED}, which indicates if the permission is
+ * currently denied or granted.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ * @see #setPermissionGrantState(ComponentName, String, String, int)
+ * @see PackageManager#checkPermission(String, String)
+ * @see #setDelegatedScopes
+ * @see #DELEGATION_PERMISSION_GRANT
+ */
+ public int getPermissionGrantState(@Nullable ComponentName admin, String packageName,
+ String permission) {
+ throwIfParentInstance("getPermissionGrantState");
+ try {
+ return mService.getPermissionGrantState(admin, mContext.getPackageName(), packageName,
+ permission);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether it is possible for the caller to initiate provisioning of a managed profile
+ * or device, setting itself as the device or profile owner.
+ *
+ * @param action One of {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE}.
+ * @return whether provisioning a managed profile or device is possible.
+ * @throws IllegalArgumentException if the supplied action is not valid.
+ */
+ public boolean isProvisioningAllowed(@NonNull String action) {
+ throwIfParentInstance("isProvisioningAllowed");
+ try {
+ return mService.isProvisioningAllowed(action, mContext.getPackageName());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks whether it is possible to initiate provisioning a managed device,
+ * profile or user, setting the given package as owner.
+ *
+ * @param action One of {@link #ACTION_PROVISION_MANAGED_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_PROFILE},
+ * {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE},
+ * {@link #ACTION_PROVISION_MANAGED_USER}
+ * @param packageName The package of the component that would be set as device, user, or profile
+ * owner.
+ * @return A {@link ProvisioningPreCondition} value indicating whether provisioning is allowed.
+ * @hide
+ */
+ public @ProvisioningPreCondition int checkProvisioningPreCondition(
+ String action, @NonNull String packageName) {
+ try {
+ return mService.checkProvisioningPreCondition(action, packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return if this user is a managed profile of another user. An admin can become the profile
+ * owner of a managed profile with {@link #ACTION_PROVISION_MANAGED_PROFILE} and of a managed
+ * user with {@link #createAndManageUser}
+ * @param admin Which profile owner this request is associated with.
+ * @return if this user is a managed profile of another user.
+ */
+ public boolean isManagedProfile(@NonNull ComponentName admin) {
+ throwIfParentInstance("isManagedProfile");
+ try {
+ return mService.isManagedProfile(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Return if this user is a system-only user. An admin can manage a device from a system only
+ * user by calling {@link #ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE}.
+ * @param admin Which device owner this request is associated with.
+ * @return if this user is a system-only user.
+ */
+ public boolean isSystemOnlyUser(@NonNull ComponentName admin) {
+ try {
+ return mService.isSystemOnlyUser(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to get the MAC address of the Wi-Fi device.
+ *
+ * @param admin Which device owner this request is associated with.
+ * @return the MAC address of the Wi-Fi device, or null when the information is not available.
+ * (For example, Wi-Fi hasn't been enabled, or the device doesn't support Wi-Fi.)
+ * <p>
+ * The address will be in the {@code XX:XX:XX:XX:XX:XX} format.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public @Nullable String getWifiMacAddress(@NonNull ComponentName admin) {
+ throwIfParentInstance("getWifiMacAddress");
+ try {
+ return mService.getWifiMacAddress(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to reboot the device. If there is an ongoing call on the device,
+ * throws an {@link IllegalStateException}.
+ * @param admin Which device owner the request is associated with.
+ * @throws IllegalStateException if device has an ongoing call.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see TelephonyManager#CALL_STATE_IDLE
+ */
+ public void reboot(@NonNull ComponentName admin) {
+ throwIfParentInstance("reboot");
+ try {
+ mService.reboot(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device admin to set the short support message. This will be displayed to the user
+ * in settings screens where funtionality has been disabled by the admin. The message should be
+ * limited to a short statement such as "This setting is disabled by your administrator. Contact
+ * someone@example.com for support." If the message is longer than 200 characters it may be
+ * truncated.
+ * <p>
+ * If the short support message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this string accordingly.
+ *
+ * @see #setLongSupportMessage
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param message Short message to be displayed to the user in settings or null to clear the
+ * existing message.
+ * @throws SecurityException if {@code admin} is not an active administrator.
+ */
+ public void setShortSupportMessage(@NonNull ComponentName admin,
+ @Nullable CharSequence message) {
+ throwIfParentInstance("setShortSupportMessage");
+ if (mService != null) {
+ try {
+ mService.setShortSupportMessage(admin, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a device admin to get the short support message.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The message set by {@link #setShortSupportMessage(ComponentName, CharSequence)} or
+ * null if no message has been set.
+ * @throws SecurityException if {@code admin} is not an active administrator.
+ */
+ public CharSequence getShortSupportMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getShortSupportMessage");
+ if (mService != null) {
+ try {
+ return mService.getShortSupportMessage(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by a device admin to set the long support message. This will be displayed to the user
+ * in the device administators settings screen.
+ * <p>
+ * If the long support message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this string accordingly.
+ *
+ * @see #setShortSupportMessage
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param message Long message to be displayed to the user in settings or null to clear the
+ * existing message.
+ * @throws SecurityException if {@code admin} is not an active administrator.
+ */
+ public void setLongSupportMessage(@NonNull ComponentName admin,
+ @Nullable CharSequence message) {
+ throwIfParentInstance("setLongSupportMessage");
+ if (mService != null) {
+ try {
+ mService.setLongSupportMessage(admin, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a device admin to get the long support message.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The message set by {@link #setLongSupportMessage(ComponentName, CharSequence)} or
+ * null if no message has been set.
+ * @throws SecurityException if {@code admin} is not an active administrator.
+ */
+ public @Nullable CharSequence getLongSupportMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getLongSupportMessage");
+ if (mService != null) {
+ try {
+ return mService.getLongSupportMessage(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by the system to get the short support message.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle user id the admin is running as.
+ * @return The message set by {@link #setShortSupportMessage(ComponentName, CharSequence)}
+ *
+ * @hide
+ */
+ public @Nullable CharSequence getShortSupportMessageForUser(@NonNull ComponentName admin,
+ int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getShortSupportMessageForUser(admin, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Called by the system to get the long support message.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle user id the admin is running as.
+ * @return The message set by {@link #setLongSupportMessage(ComponentName, CharSequence)}
+ *
+ * @hide
+ */
+ public @Nullable CharSequence getLongSupportMessageForUser(
+ @NonNull ComponentName admin, int userHandle) {
+ if (mService != null) {
+ try {
+ return mService.getLongSupportMessageForUser(admin, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called by the profile owner of a managed profile to obtain a {@link DevicePolicyManager}
+ * whose calls act on the parent profile.
+ *
+ * <p>The following methods are supported for the parent instance, all other methods will
+ * throw a SecurityException when called on the parent instance:
+ * <ul>
+ * <li>{@link #getPasswordQuality}</li>
+ * <li>{@link #setPasswordQuality}</li>
+ * <li>{@link #getPasswordMinimumLength}</li>
+ * <li>{@link #setPasswordMinimumLength}</li>
+ * <li>{@link #getPasswordMinimumUpperCase}</li>
+ * <li>{@link #setPasswordMinimumUpperCase}</li>
+ * <li>{@link #getPasswordMinimumLowerCase}</li>
+ * <li>{@link #setPasswordMinimumLowerCase}</li>
+ * <li>{@link #getPasswordMinimumLetters}</li>
+ * <li>{@link #setPasswordMinimumLetters}</li>
+ * <li>{@link #getPasswordMinimumNumeric}</li>
+ * <li>{@link #setPasswordMinimumNumeric}</li>
+ * <li>{@link #getPasswordMinimumSymbols}</li>
+ * <li>{@link #setPasswordMinimumSymbols}</li>
+ * <li>{@link #getPasswordMinimumNonLetter}</li>
+ * <li>{@link #setPasswordMinimumNonLetter}</li>
+ * <li>{@link #getPasswordHistoryLength}</li>
+ * <li>{@link #setPasswordHistoryLength}</li>
+ * <li>{@link #getPasswordExpirationTimeout}</li>
+ * <li>{@link #setPasswordExpirationTimeout}</li>
+ * <li>{@link #getPasswordExpiration}</li>
+ * <li>{@link #getPasswordMaximumLength}</li>
+ * <li>{@link #isActivePasswordSufficient}</li>
+ * <li>{@link #getCurrentFailedPasswordAttempts}</li>
+ * <li>{@link #getMaximumFailedPasswordsForWipe}</li>
+ * <li>{@link #setMaximumFailedPasswordsForWipe}</li>
+ * <li>{@link #getMaximumTimeToLock}</li>
+ * <li>{@link #setMaximumTimeToLock}</li>
+ * <li>{@link #lockNow}</li>
+ * <li>{@link #getKeyguardDisabledFeatures}</li>
+ * <li>{@link #setKeyguardDisabledFeatures}</li>
+ * <li>{@link #getTrustAgentConfiguration}</li>
+ * <li>{@link #setTrustAgentConfiguration}</li>
+ * <li>{@link #getRequiredStrongAuthTimeout}</li>
+ * <li>{@link #setRequiredStrongAuthTimeout}</li>
+ * </ul>
+ *
+ * @return a new instance of {@link DevicePolicyManager} that acts on the parent profile.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public @NonNull DevicePolicyManager getParentProfileInstance(@NonNull ComponentName admin) {
+ throwIfParentInstance("getParentProfileInstance");
+ try {
+ if (!mService.isManagedProfile(admin)) {
+ throw new SecurityException("The current user does not have a parent profile.");
+ }
+ return new DevicePolicyManager(mContext, mService, true);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to control the security logging feature.
+ *
+ * <p> Security logs contain various information intended for security auditing purposes.
+ * See {@link SecurityEvent} for details.
+ *
+ * <p><strong>Note:</strong> The device owner won't be able to retrieve security logs if there
+ * are unaffiliated secondary users or profiles on the device, regardless of whether the
+ * feature is enabled. Logs will be discarded if the internal buffer fills up while waiting for
+ * all users to become affiliated. Therefore it's recommended that affiliation ids are set for
+ * new users as soon as possible after provisioning via {@link #setAffiliationIds}.
+ *
+ * @param admin Which device owner this request is associated with.
+ * @param enabled whether security logging should be enabled or not.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #retrieveSecurityLogs
+ */
+ public void setSecurityLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setSecurityLoggingEnabled");
+ try {
+ mService.setSecurityLoggingEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return whether security logging is enabled or not by the device owner.
+ *
+ * <p>Can only be called by the device owner, otherwise a {@link SecurityException} will be
+ * thrown.
+ *
+ * @param admin Which device owner this request is associated with.
+ * @return {@code true} if security logging is enabled by device owner, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public boolean isSecurityLoggingEnabled(@Nullable ComponentName admin) {
+ throwIfParentInstance("isSecurityLoggingEnabled");
+ try {
+ return mService.isSecurityLoggingEnabled(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to retrieve all new security logging entries since the last call to
+ * this API after device boots.
+ *
+ * <p> Access to the logs is rate limited and it will only return new logs after the device
+ * owner has been notified via {@link DeviceAdminReceiver#onSecurityLogsAvailable}.
+ *
+ * <p>If there is any other user or profile on the device, it must be affiliated with the
+ * device owner. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}
+ *
+ * @param admin Which device owner this request is associated with.
+ * @return the new batch of security logs which is a list of {@link SecurityEvent},
+ * or {@code null} if rate limitation is exceeded or if logging is currently disabled.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ * @see DeviceAdminReceiver#onSecurityLogsAvailable
+ */
+ public @Nullable List<SecurityEvent> retrieveSecurityLogs(@NonNull ComponentName admin) {
+ throwIfParentInstance("retrieveSecurityLogs");
+ try {
+ ParceledListSlice<SecurityEvent> list = mService.retrieveSecurityLogs(admin);
+ if (list != null) {
+ return list.getList();
+ } else {
+ // Rate limit exceeded.
+ return null;
+ }
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to obtain a {@link DevicePolicyManager} whose calls act on the parent
+ * profile.
+ *
+ * @hide
+ */
+ public @NonNull DevicePolicyManager getParentProfileInstance(UserInfo uInfo) {
+ mContext.checkSelfPermission(
+ android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
+ if (!uInfo.isManagedProfile()) {
+ throw new SecurityException("The user " + uInfo.id
+ + " does not have a parent profile.");
+ }
+ return new DevicePolicyManager(mContext, mService, true);
+ }
+
+ /**
+ * Called by device owners to retrieve device logs from before the device's last reboot.
+ * <p>
+ * <strong> This API is not supported on all devices. Calling this API on unsupported devices
+ * will result in {@code null} being returned. The device logs are retrieved from a RAM region
+ * which is not guaranteed to be corruption-free during power cycles, as a result be cautious
+ * about data corruption when parsing. </strong>
+ *
+ * <p>If there is any other user or profile on the device, it must be affiliated with the
+ * device owner. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}
+ *
+ * @param admin Which device owner this request is associated with.
+ * @return Device logs from before the latest reboot of the system, or {@code null} if this API
+ * is not supported on the device.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ * @see #retrieveSecurityLogs
+ */
+ public @Nullable List<SecurityEvent> retrievePreRebootSecurityLogs(
+ @NonNull ComponentName admin) {
+ throwIfParentInstance("retrievePreRebootSecurityLogs");
+ try {
+ ParceledListSlice<SecurityEvent> list = mService.retrievePreRebootSecurityLogs(admin);
+ if (list != null) {
+ return list.getList();
+ } else {
+ return null;
+ }
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to set the color used for customization. This
+ * color is used as background color of the confirm credentials screen for that user. The
+ * default color is teal (#00796B).
+ * <p>
+ * The confirm credentials screen can be created using
+ * {@link android.app.KeyguardManager#createConfirmDeviceCredentialIntent}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param color The 24bit (0xRRGGBB) representation of the color to be used.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public void setOrganizationColor(@NonNull ComponentName admin, int color) {
+ throwIfParentInstance("setOrganizationColor");
+ try {
+ // always enforce alpha channel to have 100% opacity
+ color |= 0xFF000000;
+ mService.setOrganizationColor(admin, color);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ *
+ * Sets the color used for customization.
+ *
+ * @param color The 24bit (0xRRGGBB) representation of the color to be used.
+ * @param userId which user to set the color to.
+ * @RequiresPermission(allOf = {
+ * Manifest.permission.MANAGE_USERS,
+ * Manifest.permission.INTERACT_ACROSS_USERS_FULL})
+ */
+ public void setOrganizationColorForUser(@ColorInt int color, @UserIdInt int userId) {
+ try {
+ // always enforce alpha channel to have 100% opacity
+ color |= 0xFF000000;
+ mService.setOrganizationColorForUser(color, userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to retrieve the color used for customization.
+ * This color is used as background color of the confirm credentials screen for that user.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The 24bit (0xRRGGBB) representation of the color to be used.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public @ColorInt int getOrganizationColor(@NonNull ComponentName admin) {
+ throwIfParentInstance("getOrganizationColor");
+ try {
+ return mService.getOrganizationColor(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Retrieve the customization color for a given user.
+ *
+ * @param userHandle The user id of the user we're interested in.
+ * @return The 24bit (0xRRGGBB) representation of the color to be used.
+ */
+ public @ColorInt int getOrganizationColorForUser(int userHandle) {
+ try {
+ return mService.getOrganizationColorForUser(userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the device owner or profile owner to set the name of the organization under
+ * management.
+ * <p>
+ * If the organization name needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this string accordingly.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param title The organization name or {@code null} to clear a previously set name.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public void setOrganizationName(@NonNull ComponentName admin, @Nullable CharSequence title) {
+ throwIfParentInstance("setOrganizationName");
+ try {
+ mService.setOrganizationName(admin, title);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a profile owner of a managed profile to retrieve the name of the organization under
+ * management.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return The organization name or {@code null} if none is set.
+ * @throws SecurityException if {@code admin} is not a profile owner.
+ */
+ public @Nullable CharSequence getOrganizationName(@NonNull ComponentName admin) {
+ throwIfParentInstance("getOrganizationName");
+ try {
+ return mService.getOrganizationName(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to retrieve the name of the organization managing the device.
+ *
+ * @return The organization name or {@code null} if none is set.
+ * @throws SecurityException if the caller is not the device owner, does not hold the
+ * MANAGE_USERS permission and is not the system.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ @SuppressLint("Doclava125")
+ public @Nullable CharSequence getDeviceOwnerOrganizationName() {
+ try {
+ return mService.getDeviceOwnerOrganizationName();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieve the default title message used in the confirm credentials screen for a given user.
+ *
+ * @param userHandle The user id of the user we're interested in.
+ * @return The organization name or {@code null} if none is set.
+ *
+ * @hide
+ */
+ public @Nullable CharSequence getOrganizationNameForUser(int userHandle) {
+ try {
+ return mService.getOrganizationNameForUser(userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @return the {@link UserProvisioningState} for the current user - for unmanaged users will
+ * return {@link #STATE_USER_UNMANAGED}
+ * @hide
+ */
+ @SystemApi
+ @UserProvisioningState
+ public int getUserProvisioningState() {
+ throwIfParentInstance("getUserProvisioningState");
+ if (mService != null) {
+ try {
+ return mService.getUserProvisioningState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return STATE_USER_UNMANAGED;
+ }
+
+ /**
+ * Set the {@link UserProvisioningState} for the supplied user, if they are managed.
+ *
+ * @param state to store
+ * @param userHandle for user
+ * @hide
+ */
+ public void setUserProvisioningState(@UserProvisioningState int state, int userHandle) {
+ if (mService != null) {
+ try {
+ mService.setUserProvisioningState(state, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Indicates the entity that controls the device or profile owner. Two users/profiles are
+ * affiliated if the set of ids set by their device or profile owners intersect.
+ *
+ * <p><strong>Note:</strong> Features that depend on user affiliation (such as security logging
+ * or {@link #bindDeviceAdminServiceAsUser}) won't be available when a secondary user or profile
+ * is created, until it becomes affiliated. Therefore it is recommended that the appropriate
+ * affiliation ids are set by its profile owner as soon as possible after the user/profile is
+ * created.
+ *
+ * @param admin Which profile or device owner this request is associated with.
+ * @param ids A set of opaque non-empty affiliation ids.
+ *
+ * @throws IllegalArgumentException if {@code ids} is null or contains an empty string.
+ */
+ public void setAffiliationIds(@NonNull ComponentName admin, @NonNull Set<String> ids) {
+ throwIfParentInstance("setAffiliationIds");
+ if (ids == null) {
+ throw new IllegalArgumentException("ids must not be null");
+ }
+ try {
+ mService.setAffiliationIds(admin, new ArrayList<>(ids));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the set of affiliation ids previously set via {@link #setAffiliationIds}, or an
+ * empty set if none have been set.
+ */
+ public @NonNull Set<String> getAffiliationIds(@NonNull ComponentName admin) {
+ throwIfParentInstance("getAffiliationIds");
+ try {
+ return new ArraySet<>(mService.getAffiliationIds(admin));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Returns whether this user/profile is affiliated with the device.
+ * <p>
+ * By definition, the user that the device owner runs on is always affiliated with the device.
+ * Any other user/profile is considered affiliated with the device if the set specified by its
+ * profile owner via {@link #setAffiliationIds} intersects with the device owner's.
+ *
+ */
+ public boolean isAffiliatedUser() {
+ throwIfParentInstance("isAffiliatedUser");
+ try {
+ return mService.isAffiliatedUser();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Returns whether the uninstall for {@code packageName} for the current user is in queue
+ * to be started
+ * @param packageName the package to check for
+ * @return whether the uninstall intent for {@code packageName} is pending
+ */
+ public boolean isUninstallInQueue(String packageName) {
+ try {
+ return mService.isUninstallInQueue(packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * @param packageName the package containing active DAs to be uninstalled
+ */
+ public void uninstallPackageWithActiveAdmins(String packageName) {
+ try {
+ mService.uninstallPackageWithActiveAdmins(packageName);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Remove a test admin synchronously without sending it a broadcast about being removed.
+ * If the admin is a profile owner or device owner it will still be removed.
+ *
+ * @param userHandle user id to remove the admin for.
+ * @param admin The administration compononent to remove.
+ * @throws SecurityException if the caller is not shell / root or the admin package
+ * isn't a test application see {@link ApplicationInfo#FLAG_TEST_APP}.
+ */
+ public void forceRemoveActiveAdmin(ComponentName adminReceiver, int userHandle) {
+ try {
+ mService.forceRemoveActiveAdmin(adminReceiver, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether the device has been provisioned.
+ *
+ * <p>Not for use by third-party applications.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isDeviceProvisioned() {
+ try {
+ return mService.isDeviceProvisioned();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Writes that the provisioning configuration has been applied.
+ *
+ * <p>The caller must hold the {@link android.Manifest.permission#MANAGE_USERS}
+ * permission.
+ *
+ * <p>Not for use by third-party applications.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public void setDeviceProvisioningConfigApplied() {
+ try {
+ mService.setDeviceProvisioningConfigApplied();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether the provisioning configuration has been applied.
+ *
+ * <p>The caller must hold the {@link android.Manifest.permission#MANAGE_USERS} permission.
+ *
+ * <p>Not for use by third-party applications.
+ *
+ * @return whether the provisioning configuration has been applied.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public boolean isDeviceProvisioningConfigApplied() {
+ try {
+ return mService.isDeviceProvisioningConfigApplied();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Force update user setup completed status. This API has no effect on user build.
+ * @throws {@link SecurityException} if the caller has no
+ * {@code android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS} or the caller is
+ * not {@link UserHandle#SYSTEM_USER}
+ */
+ public void forceUpdateUserSetupComplete() {
+ try {
+ mService.forceUpdateUserSetupComplete();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ private void throwIfParentInstance(String functionName) {
+ if (mParentInstance) {
+ throw new SecurityException(functionName + " cannot be called on the parent instance");
+ }
+ }
+
+ /**
+ * Allows the device owner to enable or disable the backup service.
+ *
+ * <p> Backup service manages all backup and restore mechanisms on the device. Setting this to
+ * false will prevent data from being backed up or restored.
+ *
+ * <p> Backup service is off by default when device owner is present.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled {@code true} to enable the backup service, {@code false} to disable it.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setBackupServiceEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setBackupServiceEnabled");
+ try {
+ mService.setBackupServiceEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return whether the backup service is enabled by the device owner.
+ *
+ * <p> Backup service manages all backup and restore mechanisms on the device.
+ *
+ * @return {@code true} if backup service is enabled, {@code false} otherwise.
+ * @see #setBackupServiceEnabled
+ */
+ public boolean isBackupServiceEnabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("isBackupServiceEnabled");
+ try {
+ return mService.isBackupServiceEnabled(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to control the network logging feature.
+ *
+ * <p> Network logs contain DNS lookup and connect() library call events. The following library
+ * functions are recorded while network logging is active:
+ * <ul>
+ * <li>{@code getaddrinfo()}</li>
+ * <li>{@code gethostbyname()}</li>
+ * <li>{@code connect()}</li>
+ * </ul>
+ *
+ * <p> Network logging is a low-overhead tool for forensics but it is not guaranteed to use
+ * full system call logging; event reporting is enabled by default for all processes but not
+ * strongly enforced.
+ * Events from applications using alternative implementations of libc, making direct kernel
+ * calls, or deliberately obfuscating traffic may not be recorded.
+ *
+ * <p> Some common network events may not be reported. For example:
+ * <ul>
+ * <li>Applications may hardcode IP addresses to reduce the number of DNS lookups, or use
+ * an alternative system for name resolution, and so avoid calling
+ * {@code getaddrinfo()} or {@code gethostbyname}.</li>
+ * <li>Applications may use datagram sockets for performance reasons, for example
+ * for a game client. Calling {@code connect()} is unnecessary for this kind of
+ * socket, so it will not trigger a network event.</li>
+ * </ul>
+ *
+ * <p> It is possible to directly intercept layer 3 traffic leaving the device using an
+ * always-on VPN service.
+ * See {@link #setAlwaysOnVpnPackage(ComponentName, String, boolean)}
+ * and {@link android.net.VpnService} for details.
+ *
+ * <p><strong>Note:</strong> The device owner won't be able to retrieve network logs if there
+ * are unaffiliated secondary users or profiles on the device, regardless of whether the
+ * feature is enabled. Logs will be discarded if the internal buffer fills up while waiting for
+ * all users to become affiliated. Therefore it's recommended that affiliation ids are set for
+ * new users as soon as possible after provisioning via {@link #setAffiliationIds}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled whether network logging should be enabled or not.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #retrieveNetworkLogs
+ */
+ public void setNetworkLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setNetworkLoggingEnabled");
+ try {
+ mService.setNetworkLoggingEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return whether network logging is enabled by a device owner.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Can only
+ * be {@code null} if the caller has MANAGE_USERS permission.
+ * @return {@code true} if network logging is enabled by device owner, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner and caller has
+ * no MANAGE_USERS permission
+ */
+ public boolean isNetworkLoggingEnabled(@Nullable ComponentName admin) {
+ throwIfParentInstance("isNetworkLoggingEnabled");
+ try {
+ return mService.isNetworkLoggingEnabled(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to retrieve the most recent batch of network logging events.
+ * A device owner has to provide a batchToken provided as part of
+ * {@link DeviceAdminReceiver#onNetworkLogsAvailable} callback. If the token doesn't match the
+ * token of the most recent available batch of logs, {@code null} will be returned.
+ *
+ * <p> {@link NetworkEvent} can be one of {@link DnsEvent} or {@link ConnectEvent}.
+ *
+ * <p> The list of network events is sorted chronologically, and contains at most 1200 events.
+ *
+ * <p> Access to the logs is rate limited and this method will only return a new batch of logs
+ * after the device device owner has been notified via
+ * {@link DeviceAdminReceiver#onNetworkLogsAvailable}.
+ *
+ * <p>If a secondary user or profile is created, calling this method will throw a
+ * {@link SecurityException} until all users become affiliated again. It will also no longer be
+ * possible to retrieve the network logs batch with the most recent batchToken provided
+ * by {@link DeviceAdminReceiver#onNetworkLogsAvailable}. See
+ * {@link DevicePolicyManager#setAffiliationIds}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param batchToken A token of the batch to retrieve
+ * @return A new batch of network logs which is a list of {@link NetworkEvent}. Returns
+ * {@code null} if the batch represented by batchToken is no longer available or if
+ * logging is disabled.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ * @see DeviceAdminReceiver#onNetworkLogsAvailable
+ */
+ public @Nullable List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin,
+ long batchToken) {
+ throwIfParentInstance("retrieveNetworkLogs");
+ try {
+ return mService.retrieveNetworkLogs(admin, batchToken);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to bind to a service from a profile owner or vice versa.
+ * See {@link #getBindDeviceAdminTargetUsers} for a definition of which
+ * device/profile owners are allowed to bind to services of another profile/device owner.
+ * <p>
+ * The service must be protected by {@link android.Manifest.permission#BIND_DEVICE_ADMIN}.
+ * Note that the {@link Context} used to obtain this
+ * {@link DevicePolicyManager} instance via {@link Context#getSystemService(Class)} will be used
+ * to bind to the {@link android.app.Service}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param serviceIntent Identifies the service to connect to. The Intent must specify either an
+ * explicit component name or a package name to match an
+ * {@link IntentFilter} published by a service.
+ * @param conn Receives information as the service is started and stopped in main thread. This
+ * must be a valid {@link ServiceConnection} object; it must not be {@code null}.
+ * @param flags Operation options for the binding operation. See
+ * {@link Context#bindService(Intent, ServiceConnection, int)}.
+ * @param targetUser Which user to bind to. Must be one of the users returned by
+ * {@link #getBindDeviceAdminTargetUsers}, otherwise a {@link SecurityException} will
+ * be thrown.
+ * @return If you have successfully bound to the service, {@code true} is returned;
+ * {@code false} is returned if the connection is not made and you will not
+ * receive the service object.
+ *
+ * @see Context#bindService(Intent, ServiceConnection, int)
+ * @see #getBindDeviceAdminTargetUsers(ComponentName)
+ */
+ public boolean bindDeviceAdminServiceAsUser(
+ @NonNull ComponentName admin, Intent serviceIntent, @NonNull ServiceConnection conn,
+ @Context.BindServiceFlags int flags, @NonNull UserHandle targetUser) {
+ throwIfParentInstance("bindDeviceAdminServiceAsUser");
+ // Keep this in sync with ContextImpl.bindServiceCommon.
+ try {
+ final IServiceConnection sd = mContext.getServiceDispatcher(
+ conn, mContext.getMainThreadHandler(), flags);
+ serviceIntent.prepareToLeaveProcess(mContext);
+ return mService.bindDeviceAdminServiceAsUser(admin,
+ mContext.getIApplicationThread(), mContext.getActivityToken(), serviceIntent,
+ sd, flags, targetUser.getIdentifier());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the list of target users that the calling device or profile owner can use when
+ * calling {@link #bindDeviceAdminServiceAsUser}.
+ * <p>
+ * A device owner can bind to a service from a profile owner and vice versa, provided that:
+ * <ul>
+ * <li>Both belong to the same package name.
+ * <li>Both users are affiliated. See {@link #setAffiliationIds}.
+ * </ul>
+ */
+ public @NonNull List<UserHandle> getBindDeviceAdminTargetUsers(@NonNull ComponentName admin) {
+ throwIfParentInstance("getBindDeviceAdminTargetUsers");
+ try {
+ return mService.getBindDeviceAdminTargetUsers(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to get the time at which the device owner last retrieved security
+ * logging entries.
+ *
+ * @return the time at which the device owner most recently retrieved security logging entries,
+ * in milliseconds since epoch; -1 if security logging entries were never retrieved.
+ * @throws SecurityException if the caller is not the device owner, does not hold the
+ * MANAGE_USERS permission and is not the system.
+ *
+ * @hide
+ */
+ @TestApi
+ public long getLastSecurityLogRetrievalTime() {
+ try {
+ return mService.getLastSecurityLogRetrievalTime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to get the time at which the device owner last requested a bug report.
+ *
+ * @return the time at which the device owner most recently requested a bug report, in
+ * milliseconds since epoch; -1 if a bug report was never requested.
+ * @throws SecurityException if the caller is not the device owner, does not hold the
+ * MANAGE_USERS permission and is not the system.
+ *
+ * @hide
+ */
+ @TestApi
+ public long getLastBugReportRequestTime() {
+ try {
+ return mService.getLastBugReportRequestTime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to get the time at which the device owner last retrieved network logging
+ * events.
+ *
+ * @return the time at which the device owner most recently retrieved network logging events, in
+ * milliseconds since epoch; -1 if network logging events were never retrieved.
+ * @throws SecurityException if the caller is not the device owner, does not hold the
+ * MANAGE_USERS permission and is not the system.
+ *
+ * @hide
+ */
+ @TestApi
+ public long getLastNetworkLogRetrievalTime() {
+ try {
+ return mService.getLastNetworkLogRetrievalTime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to find out whether the current user's IME was set by the device/profile
+ * owner or the user.
+ *
+ * @return {@code true} if the user's IME was set by the device or profile owner, {@code false}
+ * otherwise.
+ * @throws SecurityException if the caller is not the device owner/profile owner.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean isCurrentInputMethodSetByOwner() {
+ try {
+ return mService.isCurrentInputMethodSetByOwner();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the system to get a list of CA certificates that were installed by the device or
+ * profile owner.
+ *
+ * <p> The caller must be the target user's device owner/profile Owner or hold the
+ * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission.
+ *
+ * @param user The user for whom to retrieve information.
+ * @return list of aliases identifying CA certificates installed by the device or profile owner
+ * @throws SecurityException if the caller does not have permission to retrieve information
+ * about the given user's CA certificates.
+ *
+ * @hide
+ */
+ @TestApi
+ public List<String> getOwnerInstalledCaCerts(@NonNull UserHandle user) {
+ try {
+ return mService.getOwnerInstalledCaCerts(user).getList();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the device owner or profile owner to clear application user data of a given
+ * package. The behaviour of this is equivalent to the target application calling
+ * {@link android.app.ActivityManager#clearApplicationUserData()}.
+ *
+ * <p><strong>Note:</strong> an application can store data outside of its application data, e.g.
+ * external storage or user dictionary. This data will not be wiped by calling this API.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The name of the package which will have its user data wiped.
+ * @param listener A callback object that will inform the caller when the clearing is done.
+ * @param handler The handler indicating the thread on which the listener should be invoked.
+ * @throws SecurityException if the caller is not the device owner/profile owner.
+ * @return whether the clearing succeeded.
+ */
+ public boolean clearApplicationUserData(@NonNull ComponentName admin,
+ @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener,
+ @NonNull Handler handler) {
+ throwIfParentInstance("clearAppData");
+ try {
+ return mService.clearApplicationUserData(admin, packageName,
+ new IPackageDataObserver.Stub() {
+ public void onRemoveCompleted(String pkg, boolean succeeded) {
+ handler.post(() ->
+ listener.onApplicationUserDataCleared(pkg, succeeded));
+ }
+ });
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Callback used in {@link #clearApplicationUserData}
+ * to indicate that the clearing of an application's user data is done.
+ */
+ public interface OnClearApplicationUserDataListener {
+ /**
+ * Method invoked when clearing the application user data has completed.
+ *
+ * @param packageName The name of the package which had its user data cleared.
+ * @param succeeded Whether the clearing succeeded. Clearing fails for device administrator
+ * apps and protected system packages.
+ */
+ void onApplicationUserDataCleared(String packageName, boolean succeeded);
+ }
+}
diff --git a/android/app/admin/DevicePolicyManagerInternal.java b/android/app/admin/DevicePolicyManagerInternal.java
new file mode 100644
index 00000000..eef2f983
--- /dev/null
+++ b/android/app/admin/DevicePolicyManagerInternal.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.content.Intent;
+
+import java.util.List;
+
+/**
+ * Device policy manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class DevicePolicyManagerInternal {
+
+ /**
+ * Listener for changes in the white-listed packages to show cross-profile
+ * widgets.
+ */
+ public interface OnCrossProfileWidgetProvidersChangeListener {
+
+ /**
+ * Called when the white-listed packages to show cross-profile widgets
+ * have changed for a given user.
+ *
+ * @param profileId The profile for which the white-listed packages changed.
+ * @param packages The white-listed packages.
+ */
+ public void onCrossProfileWidgetProvidersChanged(int profileId, List<String> packages);
+ }
+
+ /**
+ * Gets the packages whose widget providers are white-listed to be
+ * available in the parent user.
+ *
+ * <p>This takes the DPMS lock. DO NOT call from PM/UM/AM with their lock held.
+ *
+ * @param profileId The profile id.
+ * @return The list of packages if such or empty list if there are
+ * no white-listed packages or the profile id is not a managed
+ * profile.
+ */
+ public abstract List<String> getCrossProfileWidgetProviders(int profileId);
+
+ /**
+ * Adds a listener for changes in the white-listed packages to show
+ * cross-profile app widgets.
+ *
+ * <p>This takes the DPMS lock. DO NOT call from PM/UM/AM with their lock held.
+ *
+ * @param listener The listener to add.
+ */
+ public abstract void addOnCrossProfileWidgetProvidersChangeListener(
+ OnCrossProfileWidgetProvidersChangeListener listener);
+
+ /**
+ * Checks if an app with given uid is an active device admin of its user and has the policy
+ * specified.
+ *
+ * <p>This takes the DPMS lock. DO NOT call from PM/UM/AM with their lock held.
+ *
+ * @param uid App uid.
+ * @param reqPolicy Required policy, for policies see {@link DevicePolicyManager}.
+ * @return true if the uid is an active admin with the given policy.
+ */
+ public abstract boolean isActiveAdminWithPolicy(int uid, int reqPolicy);
+
+ /**
+ * Creates an intent to show the admin support dialog to say that an action is disallowed by
+ * the device/profile owner.
+ *
+ * <p>This method does not take the DPMS lock. Safe to be called from anywhere.
+ * @param userId The user where the action is disallowed.
+ * @param useDefaultIfNoAdmin If true, a non-null intent will be returned, even if we couldn't
+ * find a profile/device owner.
+ * @return The intent to trigger the admin support dialog.
+ */
+ public abstract Intent createShowAdminSupportIntent(int userId, boolean useDefaultIfNoAdmin);
+
+ /**
+ * Creates an intent to show the admin support dialog showing the admin who has set a user
+ * restriction.
+ *
+ * <p>This method does not take the DPMS lock. Safe to be called from anywhere.
+ * @param userId The user where the user restriction is set.
+ * @return The intent to trigger the admin support dialog, or null if the user restriction is
+ * not enforced by the profile/device owner.
+ */
+ public abstract Intent createUserRestrictionSupportIntent(int userId, String userRestriction);
+}
diff --git a/android/app/admin/DnsEvent.java b/android/app/admin/DnsEvent.java
new file mode 100644
index 00000000..f84c5b00
--- /dev/null
+++ b/android/app/admin/DnsEvent.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class that represents a DNS lookup event initiated through the standard network stack.
+ *
+ * <p>It contains information about the originating app as well as the DNS hostname and resolved
+ * IP addresses.
+ */
+public final class DnsEvent extends NetworkEvent implements Parcelable {
+
+ /** The hostname that was looked up. */
+ private final String hostname;
+
+ /** Contains (possibly a subset of) the IP addresses returned. */
+ private final String[] ipAddresses;
+
+ /**
+ * The number of IP addresses returned from the DNS lookup event. May be different from the
+ * length of ipAddresses if there were too many addresses to log.
+ */
+ private final int ipAddressesCount;
+
+ /** @hide */
+ public DnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
+ String packageName, long timestamp) {
+ super(packageName, timestamp);
+ this.hostname = hostname;
+ this.ipAddresses = ipAddresses;
+ this.ipAddressesCount = ipAddressesCount;
+ }
+
+ private DnsEvent(Parcel in) {
+ this.hostname = in.readString();
+ this.ipAddresses = in.createStringArray();
+ this.ipAddressesCount = in.readInt();
+ this.packageName = in.readString();
+ this.timestamp = in.readLong();
+ }
+
+ /** Returns the hostname that was looked up. */
+ public String getHostname() {
+ return hostname;
+ }
+
+ /** Returns (possibly a subset of) the IP addresses returned. */
+ public List<InetAddress> getInetAddresses() {
+ if (ipAddresses == null || ipAddresses.length == 0) {
+ return Collections.emptyList();
+ }
+ final List<InetAddress> inetAddresses = new ArrayList<>(ipAddresses.length);
+ for (final String ipAddress : ipAddresses) {
+ try {
+ // ipAddress is already an address, not a host name, no DNS resolution will happen.
+ inetAddresses.add(InetAddress.getByName(ipAddress));
+ } catch (UnknownHostException e) {
+ // Should never happen as we aren't passing a host name.
+ }
+ }
+ return inetAddresses;
+ }
+
+ /**
+ * Returns the number of IP addresses returned from the DNS lookup event. May be different from
+ * the length of the list returned by {@link #getInetAddresses()} if there were too many
+ * addresses to log.
+ */
+ public int getTotalResolvedAddressCount() {
+ return ipAddressesCount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("DnsEvent(%s, %s, %d, %d, %s)", hostname,
+ (ipAddresses == null) ? "NONE" : String.join(" ", ipAddresses),
+ ipAddressesCount, timestamp, packageName);
+ }
+
+ public static final Parcelable.Creator<DnsEvent> CREATOR
+ = new Parcelable.Creator<DnsEvent>() {
+ @Override
+ public DnsEvent createFromParcel(Parcel in) {
+ if (in.readInt() != PARCEL_TOKEN_DNS_EVENT) {
+ return null;
+ }
+ return new DnsEvent(in);
+ }
+
+ @Override
+ public DnsEvent[] newArray(int size) {
+ return new DnsEvent[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // write parcel token first
+ out.writeInt(PARCEL_TOKEN_DNS_EVENT);
+ out.writeString(hostname);
+ out.writeStringArray(ipAddresses);
+ out.writeInt(ipAddressesCount);
+ out.writeString(packageName);
+ out.writeLong(timestamp);
+ }
+}
+
diff --git a/android/app/admin/NetworkEvent.java b/android/app/admin/NetworkEvent.java
new file mode 100644
index 00000000..2646c3fd
--- /dev/null
+++ b/android/app/admin/NetworkEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.content.pm.PackageManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelFormatException;
+
+/**
+ * An abstract class that represents a network event.
+ */
+public abstract class NetworkEvent implements Parcelable {
+
+ /** @hide */
+ static final int PARCEL_TOKEN_DNS_EVENT = 1;
+ /** @hide */
+ static final int PARCEL_TOKEN_CONNECT_EVENT = 2;
+
+ /** The package name of the UID that performed the query. */
+ String packageName;
+
+ /** The timestamp of the event being reported in milliseconds. */
+ long timestamp;
+
+ /** @hide */
+ NetworkEvent() {
+ //empty constructor
+ }
+
+ /** @hide */
+ NetworkEvent(String packageName, long timestamp) {
+ this.packageName = packageName;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Returns the package name of the UID that performed the query, as returned by
+ * {@link PackageManager#getNameForUid}.
+ */
+ public String getPackageName() {
+ return packageName;
+ }
+
+ /**
+ * Returns the timestamp of the event being reported in milliseconds, the difference between
+ * the time the event was reported and midnight, January 1, 1970 UTC.
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<NetworkEvent> CREATOR
+ = new Parcelable.Creator<NetworkEvent>() {
+ public NetworkEvent createFromParcel(Parcel in) {
+ final int initialPosition = in.dataPosition();
+ final int parcelToken = in.readInt();
+ // we need to move back to the position from before we read parcelToken
+ in.setDataPosition(initialPosition);
+ switch (parcelToken) {
+ case PARCEL_TOKEN_DNS_EVENT:
+ return DnsEvent.CREATOR.createFromParcel(in);
+ case PARCEL_TOKEN_CONNECT_EVENT:
+ return ConnectEvent.CREATOR.createFromParcel(in);
+ default:
+ throw new ParcelFormatException("Unexpected NetworkEvent token in parcel: "
+ + parcelToken);
+ }
+ }
+
+ public NetworkEvent[] newArray(int size) {
+ return new NetworkEvent[size];
+ }
+ };
+
+ @Override
+ public abstract void writeToParcel(Parcel out, int flags);
+}
+
diff --git a/android/app/admin/PasswordMetrics.java b/android/app/admin/PasswordMetrics.java
new file mode 100644
index 00000000..4658a474
--- /dev/null
+++ b/android/app/admin/PasswordMetrics.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class that represents the metrics of a password that are used to decide whether or not a
+ * password meets the requirements.
+ *
+ * {@hide}
+ */
+public class PasswordMetrics implements Parcelable {
+ // Maximum allowed number of repeated or ordered characters in a sequence before we'll
+ // consider it a complex PIN/password.
+ public static final int MAX_ALLOWED_SEQUENCE = 3;
+
+ public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ public int length = 0;
+ public int letters = 0;
+ public int upperCase = 0;
+ public int lowerCase = 0;
+ public int numeric = 0;
+ public int symbols = 0;
+ public int nonLetter = 0;
+
+ public PasswordMetrics() {}
+
+ public PasswordMetrics(int quality, int length) {
+ this.quality = quality;
+ this.length = length;
+ }
+
+ public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase,
+ int numeric, int symbols, int nonLetter) {
+ this(quality, length);
+ this.letters = letters;
+ this.upperCase = upperCase;
+ this.lowerCase = lowerCase;
+ this.numeric = numeric;
+ this.symbols = symbols;
+ this.nonLetter = nonLetter;
+ }
+
+ private PasswordMetrics(Parcel in) {
+ quality = in.readInt();
+ length = in.readInt();
+ letters = in.readInt();
+ upperCase = in.readInt();
+ lowerCase = in.readInt();
+ numeric = in.readInt();
+ symbols = in.readInt();
+ nonLetter = in.readInt();
+ }
+
+ public boolean isDefault() {
+ return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
+ && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
+ && numeric == 0 && symbols == 0 && nonLetter == 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(quality);
+ dest.writeInt(length);
+ dest.writeInt(letters);
+ dest.writeInt(upperCase);
+ dest.writeInt(lowerCase);
+ dest.writeInt(numeric);
+ dest.writeInt(symbols);
+ dest.writeInt(nonLetter);
+ }
+
+ public static final Parcelable.Creator<PasswordMetrics> CREATOR
+ = new Parcelable.Creator<PasswordMetrics>() {
+ public PasswordMetrics createFromParcel(Parcel in) {
+ return new PasswordMetrics(in);
+ }
+
+ public PasswordMetrics[] newArray(int size) {
+ return new PasswordMetrics[size];
+ }
+ };
+
+ public static PasswordMetrics computeForPassword(@NonNull String password) {
+ // Analyse the characters used
+ int letters = 0;
+ int upperCase = 0;
+ int lowerCase = 0;
+ int numeric = 0;
+ int symbols = 0;
+ int nonLetter = 0;
+ final int length = password.length();
+ for (int i = 0; i < length; i++) {
+ switch (categoryChar(password.charAt(i))) {
+ case CHAR_LOWER_CASE:
+ letters++;
+ lowerCase++;
+ break;
+ case CHAR_UPPER_CASE:
+ letters++;
+ upperCase++;
+ break;
+ case CHAR_DIGIT:
+ numeric++;
+ nonLetter++;
+ break;
+ case CHAR_SYMBOL:
+ symbols++;
+ nonLetter++;
+ break;
+ }
+ }
+
+ // Determine the quality of the password
+ final boolean hasNumeric = numeric > 0;
+ final boolean hasNonNumeric = (letters + symbols) > 0;
+ final int quality;
+ if (hasNonNumeric && hasNumeric) {
+ quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+ } else if (hasNonNumeric) {
+ quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+ } else if (hasNumeric) {
+ quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
+ ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+ : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
+ } else {
+ quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ }
+
+ return new PasswordMetrics(
+ quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof PasswordMetrics)) {
+ return false;
+ }
+ PasswordMetrics o = (PasswordMetrics) other;
+ return this.quality == o.quality
+ && this.length == o.length
+ && this.letters == o.letters
+ && this.upperCase == o.upperCase
+ && this.lowerCase == o.lowerCase
+ && this.numeric == o.numeric
+ && this.symbols == o.symbols
+ && this.nonLetter == o.nonLetter;
+ }
+
+ /*
+ * Returns the maximum length of a sequential characters. A sequence is defined as
+ * monotonically increasing characters with a constant interval or the same character repeated.
+ *
+ * For example:
+ * maxLengthSequence("1234") == 4
+ * maxLengthSequence("13579") == 5
+ * maxLengthSequence("1234abc") == 4
+ * maxLengthSequence("aabc") == 3
+ * maxLengthSequence("qwertyuio") == 1
+ * maxLengthSequence("@ABC") == 3
+ * maxLengthSequence(";;;;") == 4 (anything that repeats)
+ * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits)
+ *
+ * @param string the pass
+ * @return the number of sequential letters or digits
+ */
+ public static int maxLengthSequence(@NonNull String string) {
+ if (string.length() == 0) return 0;
+ char previousChar = string.charAt(0);
+ @CharacterCatagory int category = categoryChar(previousChar); //current sequence category
+ int diff = 0; //difference between two consecutive characters
+ boolean hasDiff = false; //if we are currently targeting a sequence
+ int maxLength = 0; //maximum length of a sequence already found
+ int startSequence = 0; //where the current sequence started
+ for (int current = 1; current < string.length(); current++) {
+ char currentChar = string.charAt(current);
+ @CharacterCatagory int categoryCurrent = categoryChar(currentChar);
+ int currentDiff = (int) currentChar - (int) previousChar;
+ if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
+ maxLength = Math.max(maxLength, current - startSequence);
+ startSequence = current;
+ hasDiff = false;
+ category = categoryCurrent;
+ }
+ else {
+ if(hasDiff && currentDiff != diff) {
+ maxLength = Math.max(maxLength, current - startSequence);
+ startSequence = current - 1;
+ }
+ diff = currentDiff;
+ hasDiff = true;
+ }
+ previousChar = currentChar;
+ }
+ maxLength = Math.max(maxLength, string.length() - startSequence);
+ return maxLength;
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CHAR_UPPER_CASE, CHAR_LOWER_CASE, CHAR_DIGIT, CHAR_SYMBOL})
+ private @interface CharacterCatagory {}
+ private static final int CHAR_LOWER_CASE = 0;
+ private static final int CHAR_UPPER_CASE = 1;
+ private static final int CHAR_DIGIT = 2;
+ private static final int CHAR_SYMBOL = 3;
+
+ @CharacterCatagory
+ private static int categoryChar(char c) {
+ if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
+ if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
+ if ('0' <= c && c <= '9') return CHAR_DIGIT;
+ return CHAR_SYMBOL;
+ }
+
+ private static int maxDiffCategory(@CharacterCatagory int category) {
+ switch (category) {
+ case CHAR_LOWER_CASE:
+ case CHAR_UPPER_CASE:
+ return 1;
+ case CHAR_DIGIT:
+ return 10;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/android/app/admin/SecurityLog.java b/android/app/admin/SecurityLog.java
new file mode 100644
index 00000000..2b590e0d
--- /dev/null
+++ b/android/app/admin/SecurityLog.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.util.EventLog.Event;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+
+/**
+ * Definitions for working with security logs.
+ *
+ * <p>Device owner apps can control the logging with
+ * {@link DevicePolicyManager#setSecurityLoggingEnabled}. When security logs are enabled, device
+ * owner apps receive periodic callbacks from {@link DeviceAdminReceiver#onSecurityLogsAvailable},
+ * at which time new batch of logs can be collected via
+ * {@link DevicePolicyManager#retrieveSecurityLogs}. {@link SecurityEvent} describes the type and
+ * format of security logs being collected.
+ */
+public class SecurityLog {
+
+ private static final String PROPERTY_LOGGING_ENABLED = "persist.logd.security";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TAG_ADB_SHELL_INTERACTIVE, TAG_ADB_SHELL_CMD, TAG_SYNC_RECV_FILE, TAG_SYNC_SEND_FILE,
+ TAG_APP_PROCESS_START, TAG_KEYGUARD_DISMISSED, TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
+ TAG_KEYGUARD_SECURED})
+ public @interface SECURITY_LOG_TAG {}
+
+ /**
+ * Indicate that an ADB interactive shell was opened via "adb shell".
+ * There is no extra payload in the log event.
+ */
+ public static final int TAG_ADB_SHELL_INTERACTIVE =
+ SecurityLogTags.SECURITY_ADB_SHELL_INTERACTIVE;
+ /**
+ * Indicate that an shell command was issued over ADB via "adb shell command"
+ * The log entry contains a string data of the shell command, accessible via
+ * {@link SecurityEvent#getData()}
+ */
+ public static final int TAG_ADB_SHELL_CMD = SecurityLogTags.SECURITY_ADB_SHELL_COMMAND;
+ /**
+ * Indicate that a file was pulled from the device via the adb daemon, for example via
+ * "adb pull". The log entry contains a string data of the path of the pulled file,
+ * accessible via {@link SecurityEvent#getData()}
+ */
+ public static final int TAG_SYNC_RECV_FILE = SecurityLogTags.SECURITY_ADB_SYNC_RECV;
+ /**
+ * Indicate that a file was pushed to the device via the adb daemon, for example via
+ * "adb push". The log entry contains a string data of the destination path of the
+ * pushed file, accessible via {@link SecurityEvent#getData()}
+ */
+ public static final int TAG_SYNC_SEND_FILE = SecurityLogTags.SECURITY_ADB_SYNC_SEND;
+ /**
+ * Indicate that an app process was started. The log entry contains the following
+ * information about the process encapsulated in an {@link Object} array, accessible via
+ * {@link SecurityEvent#getData()}:
+ * process name (String), exact start time (long), app Uid (integer), app Pid (integer),
+ * seinfo tag (String), SHA-256 hash of the base APK in hexadecimal (String)
+ */
+ public static final int TAG_APP_PROCESS_START = SecurityLogTags.SECURITY_APP_PROCESS_START;
+ /**
+ * Indicate that keyguard is being dismissed.
+ * There is no extra payload in the log event.
+ */
+ public static final int TAG_KEYGUARD_DISMISSED =
+ SecurityLogTags.SECURITY_KEYGUARD_DISMISSED;
+ /**
+ * Indicate that there has been an authentication attempt to dismiss the keyguard. The log entry
+ * contains the following information about the attempt encapsulated in an {@link Object} array,
+ * accessible via {@link SecurityEvent#getData()}:
+ * attempt result (integer, 1 for successful, 0 for unsuccessful), strength of auth method
+ * (integer, 1 if strong auth method was used, 0 otherwise)
+ */
+ public static final int TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT =
+ SecurityLogTags.SECURITY_KEYGUARD_DISMISS_AUTH_ATTEMPT;
+ /**
+ * Indicate that the device has been locked, either by user or by timeout.
+ * There is no extra payload in the log event.
+ */
+ public static final int TAG_KEYGUARD_SECURED = SecurityLogTags.SECURITY_KEYGUARD_SECURED;
+
+ /**
+ * Returns if security logging is enabled. Log producers should only write new logs if this is
+ * true. Under the hood this is the logical AND of whether device owner exists and whether
+ * it enables logging by setting the system property {@link #PROPERTY_LOGGING_ENABLED}.
+ * @hide
+ */
+ public static native boolean isLoggingEnabled();
+
+ /**
+ * @hide
+ */
+ public static void setLoggingEnabledProperty(boolean enabled) {
+ SystemProperties.set(PROPERTY_LOGGING_ENABLED, enabled ? "true" : "false");
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean getLoggingEnabledProperty() {
+ return SystemProperties.getBoolean(PROPERTY_LOGGING_ENABLED, false);
+ }
+
+ /**
+ * A class representing a security event log entry.
+ */
+ public static final class SecurityEvent implements Parcelable {
+ private Event mEvent;
+
+ /** @hide */
+ /*package*/ SecurityEvent(byte[] data) {
+ mEvent = Event.fromBytes(data);
+ }
+
+ /**
+ * Returns the timestamp in nano seconds when this event was logged.
+ */
+ public long getTimeNanos() {
+ return mEvent.getTimeNanos();
+ }
+
+ /**
+ * Returns the tag of this log entry, which specifies entry's semantics.
+ * Could be one of {@link SecurityLog#TAG_SYNC_RECV_FILE},
+ * {@link SecurityLog#TAG_SYNC_SEND_FILE}, {@link SecurityLog#TAG_ADB_SHELL_CMD},
+ * {@link SecurityLog#TAG_ADB_SHELL_INTERACTIVE}, {@link SecurityLog#TAG_APP_PROCESS_START},
+ * {@link SecurityLog#TAG_KEYGUARD_DISMISSED}, {@link SecurityLog#TAG_KEYGUARD_SECURED},
+ * {@link SecurityLog#TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT}.
+ */
+ public @SECURITY_LOG_TAG int getTag() {
+ return mEvent.getTag();
+ }
+
+ /**
+ * Returns the payload contained in this log entry or {@code null} if there is no payload.
+ */
+ public Object getData() {
+ return mEvent.getData();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(mEvent.getBytes());
+ }
+
+ public static final Parcelable.Creator<SecurityEvent> CREATOR =
+ new Parcelable.Creator<SecurityEvent>() {
+ @Override
+ public SecurityEvent createFromParcel(Parcel source) {
+ return new SecurityEvent(source.createByteArray());
+ }
+
+ @Override
+ public SecurityEvent[] newArray(int size) {
+ return new SecurityEvent[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SecurityEvent other = (SecurityEvent) o;
+ return mEvent.equals(other.mEvent);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int hashCode() {
+ return mEvent.hashCode();
+ }
+ }
+ /**
+ * Retrieve all security logs and return immediately.
+ * @hide
+ */
+ public static native void readEvents(Collection<SecurityEvent> output) throws IOException;
+
+ /**
+ * Retrieve all security logs since the given timestamp in nanoseconds and return immediately.
+ * @hide
+ */
+ public static native void readEventsSince(long timestamp, Collection<SecurityEvent> output)
+ throws IOException;
+
+ /**
+ * Retrieve all security logs before the last reboot. May return corrupted data due to
+ * unreliable pstore.
+ * @hide
+ */
+ public static native void readPreviousEvents(Collection<SecurityEvent> output)
+ throws IOException;
+
+ /**
+ * Retrieve all security logs whose timestamp (in nanosceonds) is equal to or greater than the
+ * given timestamp. This method will block until either the last log earlier than the given
+ * timestamp is about to be pruned, or after a 2-hour timeout has passed.
+ * @hide
+ */
+ public static native void readEventsOnWrapping(long timestamp, Collection<SecurityEvent> output)
+ throws IOException;
+
+ /**
+ * Write a log entry to the underlying storage, with a string payload.
+ * @hide
+ */
+ public static native int writeEvent(int tag, String str);
+
+ /**
+ * Write a log entry to the underlying storage, with several payloads.
+ * Supported types of payload are: integer, long, float, string plus array of supported types.
+ * @hide
+ */
+ public static native int writeEvent(int tag, Object... payloads);
+}
diff --git a/android/app/admin/SystemUpdateInfo.java b/android/app/admin/SystemUpdateInfo.java
new file mode 100644
index 00000000..fa31273e
--- /dev/null
+++ b/android/app/admin/SystemUpdateInfo.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A class containing information about a pending system update.
+ */
+public final class SystemUpdateInfo implements Parcelable {
+
+ /**
+ * Represents it is unknown whether the system update is a security patch.
+ */
+ public static final int SECURITY_PATCH_STATE_UNKNOWN = 0;
+
+ /**
+ * Represents the system update is not a security patch.
+ */
+ public static final int SECURITY_PATCH_STATE_FALSE = 1;
+
+ /**
+ * Represents the system update is a security patch.
+ */
+ public static final int SECURITY_PATCH_STATE_TRUE = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SECURITY_PATCH_STATE_FALSE, SECURITY_PATCH_STATE_TRUE, SECURITY_PATCH_STATE_UNKNOWN})
+ public @interface SecurityPatchState {}
+
+ private static final String ATTR_RECEIVED_TIME = "received-time";
+ private static final String ATTR_SECURITY_PATCH_STATE = "security-patch-state";
+ // Tag used to store original build fingerprint to detect when the update is applied.
+ private static final String ATTR_ORIGINAL_BUILD = "original-build";
+
+ private final long mReceivedTime;
+ @SecurityPatchState
+ private final int mSecurityPatchState;
+
+ private SystemUpdateInfo(long receivedTime, @SecurityPatchState int securityPatchState) {
+ this.mReceivedTime = receivedTime;
+ this.mSecurityPatchState = securityPatchState;
+ }
+
+ private SystemUpdateInfo(Parcel in) {
+ mReceivedTime = in.readLong();
+ mSecurityPatchState = in.readInt();
+ }
+
+ /** @hide */
+ @Nullable
+ public static SystemUpdateInfo of(long receivedTime) {
+ return receivedTime == -1
+ ? null : new SystemUpdateInfo(receivedTime, SECURITY_PATCH_STATE_UNKNOWN);
+ }
+
+ /** @hide */
+ @Nullable
+ public static SystemUpdateInfo of(long receivedTime, boolean isSecurityPatch) {
+ return receivedTime == -1 ? null : new SystemUpdateInfo(receivedTime,
+ isSecurityPatch ? SECURITY_PATCH_STATE_TRUE : SECURITY_PATCH_STATE_FALSE);
+ }
+
+ /**
+ * Gets time when the update was first available in milliseconds since midnight, January 1,
+ * 1970 UTC.
+ * @return Time in milliseconds as given by {@link System#currentTimeMillis()}
+ */
+ public long getReceivedTime() {
+ return mReceivedTime;
+ }
+
+ /**
+ * Gets whether the update is a security patch.
+ * @return {@link #SECURITY_PATCH_STATE_FALSE}, {@link #SECURITY_PATCH_STATE_TRUE}, or
+ * {@link #SECURITY_PATCH_STATE_UNKNOWN}.
+ */
+ @SecurityPatchState
+ public int getSecurityPatchState() {
+ return mSecurityPatchState;
+ }
+
+ public static final Creator<SystemUpdateInfo> CREATOR =
+ new Creator<SystemUpdateInfo>() {
+ @Override
+ public SystemUpdateInfo createFromParcel(Parcel in) {
+ return new SystemUpdateInfo(in);
+ }
+
+ @Override
+ public SystemUpdateInfo[] newArray(int size) {
+ return new SystemUpdateInfo[size];
+ }
+ };
+
+ /** @hide */
+ public void writeToXml(XmlSerializer out, String tag) throws IOException {
+ out.startTag(null, tag);
+ out.attribute(null, ATTR_RECEIVED_TIME, String.valueOf(mReceivedTime));
+ out.attribute(null, ATTR_SECURITY_PATCH_STATE, String.valueOf(mSecurityPatchState));
+ out.attribute(null, ATTR_ORIGINAL_BUILD , Build.FINGERPRINT);
+ out.endTag(null, tag);
+ }
+
+ /** @hide */
+ @Nullable
+ public static SystemUpdateInfo readFromXml(XmlPullParser parser) {
+ // If an OTA has been applied (build fingerprint has changed), discard stale info.
+ final String buildFingerprint = parser.getAttributeValue(null, ATTR_ORIGINAL_BUILD );
+ if (!Build.FINGERPRINT.equals(buildFingerprint)) {
+ return null;
+ }
+ final long receivedTime =
+ Long.parseLong(parser.getAttributeValue(null, ATTR_RECEIVED_TIME));
+ final int securityPatchState =
+ Integer.parseInt(parser.getAttributeValue(null, ATTR_SECURITY_PATCH_STATE));
+ return new SystemUpdateInfo(receivedTime, securityPatchState);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(getReceivedTime());
+ dest.writeInt(getSecurityPatchState());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SystemUpdateInfo (receivedTime = %d, securityPatchState = %s)",
+ mReceivedTime, securityPatchStateToString(mSecurityPatchState));
+ }
+
+ private static String securityPatchStateToString(@SecurityPatchState int state) {
+ switch (state) {
+ case SECURITY_PATCH_STATE_FALSE:
+ return "false";
+ case SECURITY_PATCH_STATE_TRUE:
+ return "true";
+ case SECURITY_PATCH_STATE_UNKNOWN:
+ return "unknown";
+ default:
+ throw new IllegalArgumentException("Unrecognized security patch state: " + state);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SystemUpdateInfo that = (SystemUpdateInfo) o;
+ return mReceivedTime == that.mReceivedTime
+ && mSecurityPatchState == that.mSecurityPatchState;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mReceivedTime, mSecurityPatchState);
+ }
+}
diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java
new file mode 100644
index 00000000..995d98a7
--- /dev/null
+++ b/android/app/admin/SystemUpdatePolicy.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class that represents a local system update policy set by the device owner.
+ *
+ * @see DevicePolicyManager#setSystemUpdatePolicy
+ * @see DevicePolicyManager#getSystemUpdatePolicy
+ */
+public class SystemUpdatePolicy implements Parcelable {
+
+ /** @hide */
+ @IntDef({
+ TYPE_INSTALL_AUTOMATIC,
+ TYPE_INSTALL_WINDOWED,
+ TYPE_POSTPONE})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SystemUpdatePolicyType {}
+
+ /**
+ * Unknown policy type, used only internally.
+ */
+ private static final int TYPE_UNKNOWN = -1;
+
+ /**
+ * Install system update automatically as soon as one is available.
+ */
+ public static final int TYPE_INSTALL_AUTOMATIC = 1;
+
+ /**
+ * Install system update automatically within a daily maintenance window. An update can be
+ * delayed for a maximum of 30 days, after which the policy will no longer be effective and the
+ * system will revert back to its normal behavior as if no policy were set.
+ *
+ * <p>After this policy expires, resetting it to any policy other than
+ * {@link #TYPE_INSTALL_AUTOMATIC} will produce no effect, as the 30-day maximum delay has
+ * already been used up.
+ * The {@link #TYPE_INSTALL_AUTOMATIC} policy will still take effect to install the delayed
+ * system update immediately.
+ *
+ * <p>Re-applying this policy or changing it to {@link #TYPE_POSTPONE} within the 30-day period
+ * will <i>not</i> extend policy expiration.
+ * However, the expiration will be recalculated when a new system update is made available.
+ */
+ public static final int TYPE_INSTALL_WINDOWED = 2;
+
+ /**
+ * Incoming system updates (except for security updates) will be blocked for a maximum of 30
+ * days, after which the policy will no longer be effective and the system will revert back to
+ * its normal behavior as if no policy were set.
+ *
+ * <p><b>Note:</b> security updates (e.g. monthly security patches) may <i>not</i> be affected
+ * by this policy, depending on the policy set by the device manufacturer and carrier.
+ *
+ * <p>After this policy expires, resetting it to any policy other than
+ * {@link #TYPE_INSTALL_AUTOMATIC} will produce no effect, as the 30-day maximum delay has
+ * already been used up.
+ * The {@link #TYPE_INSTALL_AUTOMATIC} policy will still take effect to install the delayed
+ * system update immediately.
+ *
+ * <p>Re-applying this policy or changing it to {@link #TYPE_INSTALL_WINDOWED} within the 30-day
+ * period will <i>not</i> extend policy expiration.
+ * However, the expiration will be recalculated when a new system update is made available.
+ */
+ public static final int TYPE_POSTPONE = 3;
+
+ private static final String KEY_POLICY_TYPE = "policy_type";
+ private static final String KEY_INSTALL_WINDOW_START = "install_window_start";
+ private static final String KEY_INSTALL_WINDOW_END = "install_window_end";
+ /**
+ * The upper boundary of the daily maintenance window: 24 * 60 minutes.
+ */
+ private static final int WINDOW_BOUNDARY = 24 * 60;
+
+ @SystemUpdatePolicyType
+ private int mPolicyType;
+
+ private int mMaintenanceWindowStart;
+ private int mMaintenanceWindowEnd;
+
+
+ private SystemUpdatePolicy() {
+ mPolicyType = TYPE_UNKNOWN;
+ }
+
+ /**
+ * Create a policy object and set it to install update automatically as soon as one is
+ * available.
+ *
+ * @see #TYPE_INSTALL_AUTOMATIC
+ */
+ public static SystemUpdatePolicy createAutomaticInstallPolicy() {
+ SystemUpdatePolicy policy = new SystemUpdatePolicy();
+ policy.mPolicyType = TYPE_INSTALL_AUTOMATIC;
+ return policy;
+ }
+
+ /**
+ * Create a policy object and set it to: new system update will only be installed automatically
+ * when the system clock is inside a daily maintenance window. If the start and end times are
+ * the same, the window is considered to include the <i>whole 24 hours</i>. That is, updates can
+ * install at any time. If the given window in invalid, an {@link IllegalArgumentException}
+ * will be thrown. If start time is later than end time, the window is considered spanning
+ * midnight (i.e. the end time denotes a time on the next day). The maintenance window will last
+ * for 30 days, after which the system will revert back to its normal behavior as if no policy
+ * were set.
+ *
+ * @param startTime the start of the maintenance window, measured as the number of minutes from
+ * midnight in the device's local time. Must be in the range of [0, 1440).
+ * @param endTime the end of the maintenance window, measured as the number of minutes from
+ * midnight in the device's local time. Must be in the range of [0, 1440).
+ * @see #TYPE_INSTALL_WINDOWED
+ */
+ public static SystemUpdatePolicy createWindowedInstallPolicy(int startTime, int endTime) {
+ if (startTime < 0 || startTime >= WINDOW_BOUNDARY
+ || endTime < 0 || endTime >= WINDOW_BOUNDARY) {
+ throw new IllegalArgumentException("startTime and endTime must be inside [0, 1440)");
+ }
+ SystemUpdatePolicy policy = new SystemUpdatePolicy();
+ policy.mPolicyType = TYPE_INSTALL_WINDOWED;
+ policy.mMaintenanceWindowStart = startTime;
+ policy.mMaintenanceWindowEnd = endTime;
+ return policy;
+ }
+
+ /**
+ * Create a policy object and set it to block installation for a maximum period of 30 days.
+ * After expiration the system will revert back to its normal behavior as if no policy were
+ * set.
+ *
+ * <p><b>Note: </b> security updates (e.g. monthly security patches) will <i>not</i> be affected
+ * by this policy.
+ *
+ * @see #TYPE_POSTPONE
+ */
+ public static SystemUpdatePolicy createPostponeInstallPolicy() {
+ SystemUpdatePolicy policy = new SystemUpdatePolicy();
+ policy.mPolicyType = TYPE_POSTPONE;
+ return policy;
+ }
+
+ /**
+ * Returns the type of system update policy.
+ *
+ * @return an integer, either one of {@link #TYPE_INSTALL_AUTOMATIC},
+ * {@link #TYPE_INSTALL_WINDOWED} and {@link #TYPE_POSTPONE}, or -1 if no policy has been set.
+ */
+ @SystemUpdatePolicyType
+ public int getPolicyType() {
+ return mPolicyType;
+ }
+
+ /**
+ * Get the start of the maintenance window.
+ *
+ * @return the start of the maintenance window measured as the number of minutes from midnight,
+ * or -1 if the policy does not have a maintenance window.
+ */
+ public int getInstallWindowStart() {
+ if (mPolicyType == TYPE_INSTALL_WINDOWED) {
+ return mMaintenanceWindowStart;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Get the end of the maintenance window.
+ *
+ * @return the end of the maintenance window measured as the number of minutes from midnight,
+ * or -1 if the policy does not have a maintenance window.
+ */
+ public int getInstallWindowEnd() {
+ if (mPolicyType == TYPE_INSTALL_WINDOWED) {
+ return mMaintenanceWindowEnd;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Return if this object represents a valid policy.
+ * @hide
+ */
+ public boolean isValid() {
+ if (mPolicyType == TYPE_INSTALL_AUTOMATIC || mPolicyType == TYPE_POSTPONE) {
+ return true;
+ } else if (mPolicyType == TYPE_INSTALL_WINDOWED) {
+ return mMaintenanceWindowStart >= 0 && mMaintenanceWindowStart < WINDOW_BOUNDARY
+ && mMaintenanceWindowEnd >= 0 && mMaintenanceWindowEnd < WINDOW_BOUNDARY;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SystemUpdatePolicy (type: %d, windowStart: %d, windowEnd: %d)",
+ mPolicyType, mMaintenanceWindowStart, mMaintenanceWindowEnd);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPolicyType);
+ dest.writeInt(mMaintenanceWindowStart);
+ dest.writeInt(mMaintenanceWindowEnd);
+ }
+
+ public static final Parcelable.Creator<SystemUpdatePolicy> CREATOR =
+ new Parcelable.Creator<SystemUpdatePolicy>() {
+
+ @Override
+ public SystemUpdatePolicy createFromParcel(Parcel source) {
+ SystemUpdatePolicy policy = new SystemUpdatePolicy();
+ policy.mPolicyType = source.readInt();
+ policy.mMaintenanceWindowStart = source.readInt();
+ policy.mMaintenanceWindowEnd = source.readInt();
+ return policy;
+ }
+
+ @Override
+ public SystemUpdatePolicy[] newArray(int size) {
+ return new SystemUpdatePolicy[size];
+ }
+ };
+
+
+ /**
+ * @hide
+ */
+ public static SystemUpdatePolicy restoreFromXml(XmlPullParser parser) {
+ try {
+ SystemUpdatePolicy policy = new SystemUpdatePolicy();
+ String value = parser.getAttributeValue(null, KEY_POLICY_TYPE);
+ if (value != null) {
+ policy.mPolicyType = Integer.parseInt(value);
+
+ value = parser.getAttributeValue(null, KEY_INSTALL_WINDOW_START);
+ if (value != null) {
+ policy.mMaintenanceWindowStart = Integer.parseInt(value);
+ }
+ value = parser.getAttributeValue(null, KEY_INSTALL_WINDOW_END);
+ if (value != null) {
+ policy.mMaintenanceWindowEnd = Integer.parseInt(value);
+ }
+ return policy;
+ }
+ } catch (NumberFormatException e) {
+ // Fail through
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public void saveToXml(XmlSerializer out) throws IOException {
+ out.attribute(null, KEY_POLICY_TYPE, Integer.toString(mPolicyType));
+ out.attribute(null, KEY_INSTALL_WINDOW_START, Integer.toString(mMaintenanceWindowStart));
+ out.attribute(null, KEY_INSTALL_WINDOW_END, Integer.toString(mMaintenanceWindowEnd));
+ }
+}
+
diff --git a/android/app/assist/AssistContent.java b/android/app/assist/AssistContent.java
new file mode 100644
index 00000000..1c9f5734
--- /dev/null
+++ b/android/app/assist/AssistContent.java
@@ -0,0 +1,219 @@
+package android.app.assist;
+
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Holds information about the content an application is viewing, to hand to an
+ * assistant at the user's request. This is filled in by
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}.
+ */
+public class AssistContent implements Parcelable {
+ private boolean mIsAppProvidedIntent = false;
+ private boolean mIsAppProvidedWebUri = false;
+ private Intent mIntent;
+ private String mStructuredData;
+ private ClipData mClipData;
+ private Uri mUri;
+ private final Bundle mExtras;
+
+ public AssistContent() {
+ mExtras = new Bundle();
+ }
+
+ /**
+ * @hide
+ * Called by {@link android.app.ActivityThread} to set the default Intent based on
+ * {@link android.app.Activity#getIntent Activity.getIntent}.
+ *
+ * <p>Automatically populates {@link #mUri} if that Intent is an {@link Intent#ACTION_VIEW}
+ * of a web (http or https scheme) URI.</p>
+ */
+ public void setDefaultIntent(Intent intent) {
+ mIntent = intent;
+ mIsAppProvidedIntent = false;
+ mIsAppProvidedWebUri = false;
+ mUri = null;
+ if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) {
+ mUri = uri;
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the Intent associated with the content, describing the current top-level context of
+ * the activity. If this contains a reference to a piece of data related to the activity,
+ * be sure to set {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} so the accessibility
+ * service can access it.
+ */
+ public void setIntent(Intent intent) {
+ mIsAppProvidedIntent = true;
+ mIntent = intent;
+ }
+
+ /**
+ * Returns the current {@link #setIntent} if one is set, else the default Intent obtained from
+ * {@link android.app.Activity#getIntent Activity.getIntent}. Can be modified in-place.
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Returns whether or not the current Intent was explicitly provided in
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}. If not,
+ * the Intent was automatically set based on
+ * {@link android.app.Activity#getIntent Activity.getIntent}.
+ */
+ public boolean isAppProvidedIntent() {
+ return mIsAppProvidedIntent;
+ }
+
+ /**
+ * Optional additional content items that are involved with
+ * the current UI. Access to this content will be granted to the assistant as if you
+ * are sending it through an Intent with {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}.
+ */
+ public void setClipData(ClipData clip) {
+ mClipData = clip;
+ }
+
+ /**
+ * Return the current {@link #setClipData}, which you can modify in-place.
+ */
+ public ClipData getClipData() {
+ return mClipData;
+ }
+
+ /**
+ * Sets optional structured data regarding the content being viewed. The provided data
+ * must be a string represented with <a href="http://json-ld.org/">JSON-LD</a> using the
+ * <a href="http://schema.org/">schema.org</a> vocabulary.
+ */
+ public void setStructuredData(String structuredData) {
+ mStructuredData = structuredData;
+ }
+
+ /**
+ * Returns the current {@link #setStructuredData}.
+ */
+ public String getStructuredData() {
+ return mStructuredData;
+ }
+
+ /**
+ * Set a web URI associated with the current data being shown to the user.
+ * This URI could be opened in a web browser, or in the app as an
+ * {@link Intent#ACTION_VIEW} Intent, to show the same data that is currently
+ * being displayed by it. The URI here should be something that is transportable
+ * off the device into other environments to acesss the same data as is currently
+ * being shown in the app; if the app does not have such a representation, it should
+ * leave the null and only report the local intent and clip data.
+ */
+ public void setWebUri(Uri uri) {
+ mIsAppProvidedWebUri = true;
+ mUri = uri;
+ }
+
+ /**
+ * Return the content's web URI as per {@link #setWebUri(android.net.Uri)}, or null if
+ * there is none.
+ */
+ public Uri getWebUri() {
+ return mUri;
+ }
+
+ /**
+ * Returns whether or not the current {@link #getWebUri} was explicitly provided in
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}. If not,
+ * the Intent was automatically set based on
+ * {@link android.app.Activity#getIntent Activity.getIntent}.
+ */
+ public boolean isAppProvidedWebUri() {
+ return mIsAppProvidedWebUri;
+ }
+
+ /**
+ * Return Bundle for extra vendor-specific data that can be modified and examined.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ AssistContent(Parcel in) {
+ if (in.readInt() != 0) {
+ mIntent = Intent.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ mClipData = ClipData.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ mStructuredData = in.readString();
+ }
+ mIsAppProvidedIntent = in.readInt() == 1;
+ mExtras = in.readBundle();
+ mIsAppProvidedWebUri = in.readInt() == 1;
+ }
+
+ void writeToParcelInternal(Parcel dest, int flags) {
+ if (mIntent != null) {
+ dest.writeInt(1);
+ mIntent.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mClipData != null) {
+ dest.writeInt(1);
+ mClipData.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mUri != null) {
+ dest.writeInt(1);
+ mUri.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mStructuredData != null) {
+ dest.writeInt(1);
+ dest.writeString(mStructuredData);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(mIsAppProvidedIntent ? 1 : 0);
+ dest.writeBundle(mExtras);
+ dest.writeInt(mIsAppProvidedWebUri ? 1 : 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ writeToParcelInternal(dest, flags);
+ }
+
+ public static final Parcelable.Creator<AssistContent> CREATOR
+ = new Parcelable.Creator<AssistContent>() {
+ public AssistContent createFromParcel(Parcel in) {
+ return new AssistContent(in);
+ }
+
+ public AssistContent[] newArray(int size) {
+ return new AssistContent[size];
+ }
+ };
+}
diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java
new file mode 100644
index 00000000..55c22de5
--- /dev/null
+++ b/android/app/assist/AssistStructure.java
@@ -0,0 +1,2164 @@
+package android.app.assist;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.BadParcelableException;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PooledStringReader;
+import android.os.PooledStringWriter;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.autofill.FillRequest;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewStructure;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.ViewStructure.HtmlInfo.Builder;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Assist data automatically created by the platform's implementation of assist and autofill.
+ *
+ * <p>The structure is used for assist purposes when created by
+ * {@link android.app.Activity#onProvideAssistData}, {@link View#onProvideStructure(ViewStructure)},
+ * or {@link View#onProvideVirtualStructure(ViewStructure)}.
+ *
+ * <p>The structure is used for autofill purposes when created by
+ * {@link View#onProvideAutofillStructure(ViewStructure, int)},
+ * or {@link View#onProvideAutofillVirtualStructure(ViewStructure, int)}.
+ *
+ * <p>For performance reasons, some properties of the assist data might be available just for assist
+ * or autofill purposes; in those case, the property availability will be document in its javadoc.
+ */
+public class AssistStructure implements Parcelable {
+ static final String TAG = "AssistStructure";
+
+ static final boolean DEBUG_PARCEL = false;
+ static final boolean DEBUG_PARCEL_CHILDREN = false;
+ static final boolean DEBUG_PARCEL_TREE = false;
+
+ static final int VALIDATE_WINDOW_TOKEN = 0x11111111;
+ static final int VALIDATE_VIEW_TOKEN = 0x22222222;
+
+ boolean mHaveData;
+
+ ComponentName mActivityComponent;
+ private boolean mIsHomeActivity;
+ private int mFlags;
+
+ final ArrayList<WindowNode> mWindowNodes = new ArrayList<>();
+
+ final ArrayList<ViewNodeBuilder> mPendingAsyncChildren = new ArrayList<>();
+
+ SendChannel mSendChannel;
+ IBinder mReceiveChannel;
+
+ Rect mTmpRect = new Rect();
+
+ boolean mSanitizeOnWrite = false;
+ private long mAcquisitionStartTime;
+ private long mAcquisitionEndTime;
+
+ static final int TRANSACTION_XFER = Binder.FIRST_CALL_TRANSACTION+1;
+ static final String DESCRIPTOR = "android.app.AssistStructure";
+
+ /** @hide */
+ public void setAcquisitionStartTime(long acquisitionStartTime) {
+ mAcquisitionStartTime = acquisitionStartTime;
+ }
+
+ /** @hide */
+ public void setAcquisitionEndTime(long acquisitionEndTime) {
+ mAcquisitionEndTime = acquisitionEndTime;
+ }
+
+ /**
+ * @hide
+ * Set the home activity flag.
+ */
+ public void setHomeActivity(boolean isHomeActivity) {
+ mIsHomeActivity = isHomeActivity;
+ }
+
+ /**
+ * Returns the time when the activity started generating assist data to build the
+ * AssistStructure. The time is as specified by {@link SystemClock#uptimeMillis()}.
+ *
+ * @see #getAcquisitionEndTime()
+ * @return Returns the acquisition start time of the assist data, in milliseconds.
+ */
+ public long getAcquisitionStartTime() {
+ ensureData();
+ return mAcquisitionStartTime;
+ }
+
+ /**
+ * Returns the time when the activity finished generating assist data to build the
+ * AssistStructure. The time is as specified by {@link SystemClock#uptimeMillis()}.
+ *
+ * @see #getAcquisitionStartTime()
+ * @return Returns the acquisition end time of the assist data, in milliseconds.
+ */
+ public long getAcquisitionEndTime() {
+ ensureData();
+ return mAcquisitionEndTime;
+ }
+
+ final static class SendChannel extends Binder {
+ volatile AssistStructure mAssistStructure;
+
+ SendChannel(AssistStructure as) {
+ mAssistStructure = as;
+ }
+
+ @Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ if (code == TRANSACTION_XFER) {
+ AssistStructure as = mAssistStructure;
+ if (as == null) {
+ return true;
+ }
+
+ data.enforceInterface(DESCRIPTOR);
+ IBinder token = data.readStrongBinder();
+ if (DEBUG_PARCEL) Log.d(TAG, "Request for data on " + as
+ + " using token " + token);
+ if (token != null) {
+ if (DEBUG_PARCEL) Log.d(TAG, "Resuming partial write of " + token);
+ if (token instanceof ParcelTransferWriter) {
+ ParcelTransferWriter xfer = (ParcelTransferWriter)token;
+ xfer.writeToParcel(as, reply);
+ return true;
+ }
+ Log.w(TAG, "Caller supplied bad token type: " + token);
+ // Don't write anything; this is the end of the data.
+ return true;
+ }
+ //long start = SystemClock.uptimeMillis();
+ ParcelTransferWriter xfer = new ParcelTransferWriter(as, reply);
+ xfer.writeToParcel(as, reply);
+ //Log.i(TAG, "Time to parcel: " + (SystemClock.uptimeMillis()-start) + "ms");
+ return true;
+ } else {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ }
+
+ final static class ViewStackEntry {
+ ViewNode node;
+ int curChild;
+ int numChildren;
+ }
+
+ final static class ParcelTransferWriter extends Binder {
+ final boolean mWriteStructure;
+ int mCurWindow;
+ int mNumWindows;
+ final ArrayList<ViewStackEntry> mViewStack = new ArrayList<>();
+ ViewStackEntry mCurViewStackEntry;
+ int mCurViewStackPos;
+ int mNumWrittenWindows;
+ int mNumWrittenViews;
+ final float[] mTmpMatrix = new float[9];
+ final boolean mSanitizeOnWrite;
+
+ ParcelTransferWriter(AssistStructure as, Parcel out) {
+ mSanitizeOnWrite = as.mSanitizeOnWrite;
+ mWriteStructure = as.waitForReady();
+ ComponentName.writeToParcel(as.mActivityComponent, out);
+ out.writeInt(as.mFlags);
+ out.writeLong(as.mAcquisitionStartTime);
+ out.writeLong(as.mAcquisitionEndTime);
+ mNumWindows = as.mWindowNodes.size();
+ if (mWriteStructure && mNumWindows > 0) {
+ out.writeInt(mNumWindows);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ void writeToParcel(AssistStructure as, Parcel out) {
+ int start = out.dataPosition();
+ mNumWrittenWindows = 0;
+ mNumWrittenViews = 0;
+ boolean more = writeToParcelInner(as, out);
+ Log.i(TAG, "Flattened " + (more ? "partial" : "final") + " assist data: "
+ + (out.dataPosition() - start)
+ + " bytes, containing " + mNumWrittenWindows + " windows, "
+ + mNumWrittenViews + " views");
+ }
+
+ boolean writeToParcelInner(AssistStructure as, Parcel out) {
+ if (mNumWindows == 0) {
+ return false;
+ }
+ if (DEBUG_PARCEL) Log.d(TAG, "Creating PooledStringWriter @ " + out.dataPosition());
+ PooledStringWriter pwriter = new PooledStringWriter(out);
+ while (writeNextEntryToParcel(as, out, pwriter)) {
+ // If the parcel is above the IPC limit, then we are getting too
+ // large for a single IPC so stop here and let the caller come back when it
+ // is ready for more.
+ if (out.dataSize() > IBinder.MAX_IPC_SIZE) {
+ if (DEBUG_PARCEL) Log.d(TAG, "Assist data size is " + out.dataSize()
+ + " @ pos " + out.dataPosition() + "; returning partial result");
+ out.writeInt(0);
+ out.writeStrongBinder(this);
+ if (DEBUG_PARCEL) Log.d(TAG, "Finishing PooledStringWriter @ "
+ + out.dataPosition() + ", size " + pwriter.getStringCount());
+ pwriter.finish();
+ return true;
+ }
+ }
+ if (DEBUG_PARCEL) Log.d(TAG, "Finishing PooledStringWriter @ "
+ + out.dataPosition() + ", size " + pwriter.getStringCount());
+ pwriter.finish();
+ mViewStack.clear();
+ return false;
+ }
+
+ void pushViewStackEntry(ViewNode node, int pos) {
+ ViewStackEntry entry;
+ if (pos >= mViewStack.size()) {
+ entry = new ViewStackEntry();
+ mViewStack.add(entry);
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "New stack entry at " + pos + ": " + entry);
+ } else {
+ entry = mViewStack.get(pos);
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Existing stack entry at " + pos + ": " + entry);
+ }
+ entry.node = node;
+ entry.numChildren = node.getChildCount();
+ entry.curChild = 0;
+ mCurViewStackEntry = entry;
+ }
+
+ void writeView(ViewNode child, Parcel out, PooledStringWriter pwriter, int levelAdj) {
+ if (DEBUG_PARCEL) Log.d(TAG, "write view: at " + out.dataPosition()
+ + ", windows=" + mNumWrittenWindows
+ + ", views=" + mNumWrittenViews
+ + ", level=" + (mCurViewStackPos+levelAdj));
+ out.writeInt(VALIDATE_VIEW_TOKEN);
+ int flags = child.writeSelfToParcel(out, pwriter, mSanitizeOnWrite, mTmpMatrix);
+ mNumWrittenViews++;
+ // If the child has children, push it on the stack to write them next.
+ if ((flags&ViewNode.FLAGS_HAS_CHILDREN) != 0) {
+ if (DEBUG_PARCEL_TREE || DEBUG_PARCEL_CHILDREN) Log.d(TAG,
+ "Preparing to write " + child.mChildren.length
+ + " children: @ #" + mNumWrittenViews
+ + ", level " + (mCurViewStackPos+levelAdj));
+ out.writeInt(child.mChildren.length);
+ int pos = ++mCurViewStackPos;
+ pushViewStackEntry(child, pos);
+ }
+ }
+
+ boolean writeNextEntryToParcel(AssistStructure as, Parcel out, PooledStringWriter pwriter) {
+ // Write next view node if appropriate.
+ if (mCurViewStackEntry != null) {
+ if (mCurViewStackEntry.curChild < mCurViewStackEntry.numChildren) {
+ // Write the next child in the current view.
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Writing child #"
+ + mCurViewStackEntry.curChild + " in " + mCurViewStackEntry.node);
+ ViewNode child = mCurViewStackEntry.node.mChildren[mCurViewStackEntry.curChild];
+ mCurViewStackEntry.curChild++;
+ writeView(child, out, pwriter, 1);
+ return true;
+ }
+
+ // We are done writing children of the current view; pop off the stack.
+ do {
+ int pos = --mCurViewStackPos;
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Done with " + mCurViewStackEntry.node
+ + "; popping up to " + pos);
+ if (pos < 0) {
+ // Reached the last view; step to next window.
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Done with view hierarchy!");
+ mCurViewStackEntry = null;
+ break;
+ }
+ mCurViewStackEntry = mViewStack.get(pos);
+ } while (mCurViewStackEntry.curChild >= mCurViewStackEntry.numChildren);
+ return true;
+ }
+
+ // Write the next window if appropriate.
+ int pos = mCurWindow;
+ if (pos < mNumWindows) {
+ WindowNode win = as.mWindowNodes.get(pos);
+ mCurWindow++;
+ if (DEBUG_PARCEL) Log.d(TAG, "write window #" + pos + ": at " + out.dataPosition()
+ + ", windows=" + mNumWrittenWindows
+ + ", views=" + mNumWrittenViews);
+ out.writeInt(VALIDATE_WINDOW_TOKEN);
+ win.writeSelfToParcel(out, pwriter, mTmpMatrix);
+ mNumWrittenWindows++;
+ ViewNode root = win.mRoot;
+ mCurViewStackPos = 0;
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Writing initial root view " + root);
+ writeView(root, out, pwriter, 0);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ final class ParcelTransferReader {
+ final float[] mTmpMatrix = new float[9];
+ PooledStringReader mStringReader;
+
+ int mNumReadWindows;
+ int mNumReadViews;
+
+ private final IBinder mChannel;
+ private IBinder mTransferToken;
+ private Parcel mCurParcel;
+
+ ParcelTransferReader(IBinder channel) {
+ mChannel = channel;
+ }
+
+ void go() {
+ fetchData();
+ mActivityComponent = ComponentName.readFromParcel(mCurParcel);
+ mFlags = mCurParcel.readInt();
+ mAcquisitionStartTime = mCurParcel.readLong();
+ mAcquisitionEndTime = mCurParcel.readLong();
+ final int N = mCurParcel.readInt();
+ if (N > 0) {
+ if (DEBUG_PARCEL) Log.d(TAG, "Creating PooledStringReader @ "
+ + mCurParcel.dataPosition());
+ mStringReader = new PooledStringReader(mCurParcel);
+ if (DEBUG_PARCEL) Log.d(TAG, "PooledStringReader size = "
+ + mStringReader.getStringCount());
+ for (int i=0; i<N; i++) {
+ mWindowNodes.add(new WindowNode(this));
+ }
+ }
+ if (DEBUG_PARCEL) Log.d(TAG, "Finished reading: at " + mCurParcel.dataPosition()
+ + ", avail=" + mCurParcel.dataAvail() + ", windows=" + mNumReadWindows
+ + ", views=" + mNumReadViews);
+ }
+
+ Parcel readParcel(int validateToken, int level) {
+ if (DEBUG_PARCEL) Log.d(TAG, "readParcel: at " + mCurParcel.dataPosition()
+ + ", avail=" + mCurParcel.dataAvail() + ", windows=" + mNumReadWindows
+ + ", views=" + mNumReadViews + ", level=" + level);
+ int token = mCurParcel.readInt();
+ if (token != 0) {
+ if (token != validateToken) {
+ throw new BadParcelableException("Got token " + Integer.toHexString(token)
+ + ", expected token " + Integer.toHexString(validateToken));
+ }
+ return mCurParcel;
+ }
+ // We have run out of partial data, need to read another batch.
+ mTransferToken = mCurParcel.readStrongBinder();
+ if (mTransferToken == null) {
+ throw new IllegalStateException(
+ "Reached end of partial data without transfer token");
+ }
+ if (DEBUG_PARCEL) Log.d(TAG, "Ran out of partial data at "
+ + mCurParcel.dataPosition() + ", token " + mTransferToken);
+ fetchData();
+ if (DEBUG_PARCEL) Log.d(TAG, "Creating PooledStringReader @ "
+ + mCurParcel.dataPosition());
+ mStringReader = new PooledStringReader(mCurParcel);
+ if (DEBUG_PARCEL) Log.d(TAG, "PooledStringReader size = "
+ + mStringReader.getStringCount());
+ if (DEBUG_PARCEL) Log.d(TAG, "readParcel: at " + mCurParcel.dataPosition()
+ + ", avail=" + mCurParcel.dataAvail() + ", windows=" + mNumReadWindows
+ + ", views=" + mNumReadViews);
+ mCurParcel.readInt();
+ return mCurParcel;
+ }
+
+ private void fetchData() {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(DESCRIPTOR);
+ data.writeStrongBinder(mTransferToken);
+ if (DEBUG_PARCEL) Log.d(TAG, "Requesting data with token " + mTransferToken);
+ if (mCurParcel != null) {
+ mCurParcel.recycle();
+ }
+ mCurParcel = Parcel.obtain();
+ try {
+ mChannel.transact(TRANSACTION_XFER, data, mCurParcel, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failure reading AssistStructure data", e);
+ throw new IllegalStateException("Failure reading AssistStructure data: " + e);
+ }
+ data.recycle();
+ mNumReadWindows = mNumReadViews = 0;
+ }
+ }
+
+ final static class ViewNodeText {
+ CharSequence mText;
+ float mTextSize;
+ int mTextStyle;
+ int mTextColor = ViewNode.TEXT_COLOR_UNDEFINED;
+ int mTextBackgroundColor = ViewNode.TEXT_COLOR_UNDEFINED;
+ int mTextSelectionStart;
+ int mTextSelectionEnd;
+ int[] mLineCharOffsets;
+ int[] mLineBaselines;
+ String mHint;
+
+ ViewNodeText() {
+ }
+
+ boolean isSimple() {
+ return mTextBackgroundColor == ViewNode.TEXT_COLOR_UNDEFINED
+ && mTextSelectionStart == 0 && mTextSelectionEnd == 0
+ && mLineCharOffsets == null && mLineBaselines == null && mHint == null;
+ }
+
+ ViewNodeText(Parcel in, boolean simple) {
+ mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mTextSize = in.readFloat();
+ mTextStyle = in.readInt();
+ mTextColor = in.readInt();
+ if (!simple) {
+ mTextBackgroundColor = in.readInt();
+ mTextSelectionStart = in.readInt();
+ mTextSelectionEnd = in.readInt();
+ mLineCharOffsets = in.createIntArray();
+ mLineBaselines = in.createIntArray();
+ mHint = in.readString();
+ }
+ }
+
+ void writeToParcel(Parcel out, boolean simple, boolean writeSensitive) {
+ TextUtils.writeToParcel(writeSensitive ? mText : "", out, 0);
+ out.writeFloat(mTextSize);
+ out.writeInt(mTextStyle);
+ out.writeInt(mTextColor);
+ if (!simple) {
+ out.writeInt(mTextBackgroundColor);
+ out.writeInt(mTextSelectionStart);
+ out.writeInt(mTextSelectionEnd);
+ out.writeIntArray(mLineCharOffsets);
+ out.writeIntArray(mLineBaselines);
+ out.writeString(mHint);
+ }
+ }
+ }
+
+ /**
+ * Describes a window in the assist data.
+ */
+ static public class WindowNode {
+ final int mX;
+ final int mY;
+ final int mWidth;
+ final int mHeight;
+ final CharSequence mTitle;
+ final int mDisplayId;
+ final ViewNode mRoot;
+
+ WindowNode(AssistStructure assist, ViewRootImpl root, boolean forAutoFill, int flags) {
+ View view = root.getView();
+ Rect rect = new Rect();
+ view.getBoundsOnScreen(rect);
+ mX = rect.left - view.getLeft();
+ mY = rect.top - view.getTop();
+ mWidth = rect.width();
+ mHeight = rect.height();
+ mTitle = root.getTitle();
+ mDisplayId = root.getDisplayId();
+ mRoot = new ViewNode();
+
+ ViewNodeBuilder builder = new ViewNodeBuilder(assist, mRoot, false);
+ if ((root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
+ if (forAutoFill) {
+ final int autofillFlags = (flags & FillRequest.FLAG_MANUAL_REQUEST) != 0
+ ? View.AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
+ view.onProvideAutofillStructure(builder, autofillFlags);
+ } else {
+ // This is a secure window, so it doesn't want a screenshot, and that
+ // means we should also not copy out its view hierarchy for Assist
+ view.onProvideStructure(builder);
+ builder.setAssistBlocked(true);
+ return;
+ }
+ }
+ if (forAutoFill) {
+ final int autofillFlags = (flags & FillRequest.FLAG_MANUAL_REQUEST) != 0
+ ? View.AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
+ view.dispatchProvideAutofillStructure(builder, autofillFlags);
+ } else {
+ view.dispatchProvideStructure(builder);
+ }
+ }
+
+ WindowNode(ParcelTransferReader reader) {
+ Parcel in = reader.readParcel(VALIDATE_WINDOW_TOKEN, 0);
+ reader.mNumReadWindows++;
+ mX = in.readInt();
+ mY = in.readInt();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+ mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mDisplayId = in.readInt();
+ mRoot = new ViewNode(reader, 0);
+ }
+
+ void writeSelfToParcel(Parcel out, PooledStringWriter pwriter, float[] tmpMatrix) {
+ out.writeInt(mX);
+ out.writeInt(mY);
+ out.writeInt(mWidth);
+ out.writeInt(mHeight);
+ TextUtils.writeToParcel(mTitle, out, 0);
+ out.writeInt(mDisplayId);
+ }
+
+ /**
+ * Returns the left edge of the window, in pixels, relative to the left
+ * edge of the screen.
+ */
+ public int getLeft() {
+ return mX;
+ }
+
+ /**
+ * Returns the top edge of the window, in pixels, relative to the top
+ * edge of the screen.
+ */
+ public int getTop() {
+ return mY;
+ }
+
+ /**
+ * Returns the total width of the window in pixels.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Returns the total height of the window in pixels.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns the title associated with the window, if it has one.
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the ID of the display this window is on, for use with
+ * {@link android.hardware.display.DisplayManager#getDisplay DisplayManager.getDisplay()}.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Returns the {@link ViewNode} containing the root content of the window.
+ */
+ public ViewNode getRootViewNode() {
+ return mRoot;
+ }
+ }
+
+ /**
+ * Describes a single view in the assist data.
+ */
+ static public class ViewNode {
+ /**
+ * Magic value for text color that has not been defined, which is very unlikely
+ * to be confused with a real text color.
+ */
+ public static final int TEXT_COLOR_UNDEFINED = 1;
+
+ public static final int TEXT_STYLE_BOLD = 1<<0;
+ public static final int TEXT_STYLE_ITALIC = 1<<1;
+ public static final int TEXT_STYLE_UNDERLINE = 1<<2;
+ public static final int TEXT_STYLE_STRIKE_THRU = 1<<3;
+
+ int mId = View.NO_ID;
+ String mIdPackage;
+ String mIdType;
+ String mIdEntry;
+
+ // TODO: once we have more flags, it might be better to store the individual
+ // fields (viewId and childId) of the field.
+ AutofillId mAutofillId;
+ @View.AutofillType int mAutofillType = View.AUTOFILL_TYPE_NONE;
+ @Nullable String[] mAutofillHints;
+ AutofillValue mAutofillValue;
+ CharSequence[] mAutofillOptions;
+ boolean mSanitized;
+ HtmlInfo mHtmlInfo;
+
+ // POJO used to override some autofill-related values when the node is parcelized.
+ // Not written to parcel.
+ AutofillOverlay mAutofillOverlay;
+
+ int mX;
+ int mY;
+ int mScrollX;
+ int mScrollY;
+ int mWidth;
+ int mHeight;
+ Matrix mMatrix;
+ float mElevation;
+ float mAlpha = 1.0f;
+
+ static final int FLAGS_DISABLED = 0x00000001;
+ static final int FLAGS_VISIBILITY_MASK = View.VISIBLE|View.INVISIBLE|View.GONE;
+ static final int FLAGS_FOCUSABLE = 0x00000010;
+ static final int FLAGS_FOCUSED = 0x00000020;
+ static final int FLAGS_SELECTED = 0x00000040;
+ static final int FLAGS_ASSIST_BLOCKED = 0x00000080;
+ static final int FLAGS_CHECKABLE = 0x00000100;
+ static final int FLAGS_CHECKED = 0x00000200;
+ static final int FLAGS_CLICKABLE = 0x00000400;
+ static final int FLAGS_LONG_CLICKABLE = 0x00000800;
+ static final int FLAGS_ACCESSIBILITY_FOCUSED = 0x00001000;
+ static final int FLAGS_ACTIVATED = 0x00002000;
+ static final int FLAGS_CONTEXT_CLICKABLE = 0x00004000;
+ static final int FLAGS_OPAQUE = 0x00008000;
+
+ // TODO: autofill data is made of many fields and ideally we should verify
+ // one-by-one to optimize what's sent over, but there isn't enough flag bits for that, we'd
+ // need to create a 'flags2' or 'autoFillFlags' field and add these flags there.
+ // So, to keep thinkg simpler for now, let's just use on flag for all of them...
+ static final int FLAGS_HAS_AUTOFILL_DATA = 0x80000000;
+ static final int FLAGS_HAS_MATRIX = 0x40000000;
+ static final int FLAGS_HAS_ALPHA = 0x20000000;
+ static final int FLAGS_HAS_ELEVATION = 0x10000000;
+ static final int FLAGS_HAS_SCROLL = 0x08000000;
+ static final int FLAGS_HAS_LARGE_COORDS = 0x04000000;
+ static final int FLAGS_HAS_CONTENT_DESCRIPTION = 0x02000000;
+ static final int FLAGS_HAS_TEXT = 0x01000000;
+ static final int FLAGS_HAS_COMPLEX_TEXT = 0x00800000;
+ static final int FLAGS_HAS_EXTRAS = 0x00400000;
+ static final int FLAGS_HAS_ID = 0x00200000;
+ static final int FLAGS_HAS_CHILDREN = 0x00100000;
+ static final int FLAGS_HAS_URL = 0x00080000;
+ static final int FLAGS_HAS_INPUT_TYPE = 0x00040000;
+ static final int FLAGS_HAS_LOCALE_LIST = 0x00010000;
+ static final int FLAGS_ALL_CONTROL = 0xfff00000;
+
+ int mFlags;
+
+ String mClassName;
+ CharSequence mContentDescription;
+
+ ViewNodeText mText;
+ int mInputType;
+ String mWebDomain;
+ Bundle mExtras;
+ LocaleList mLocaleList;
+
+ ViewNode[] mChildren;
+
+ ViewNode() {
+ }
+
+ ViewNode(ParcelTransferReader reader, int nestingLevel) {
+ final Parcel in = reader.readParcel(VALIDATE_VIEW_TOKEN, nestingLevel);
+ reader.mNumReadViews++;
+ final PooledStringReader preader = reader.mStringReader;
+ mClassName = preader.readString();
+ mFlags = in.readInt();
+ final int flags = mFlags;
+ if ((flags&FLAGS_HAS_ID) != 0) {
+ mId = in.readInt();
+ if (mId != 0) {
+ mIdEntry = preader.readString();
+ if (mIdEntry != null) {
+ mIdType = preader.readString();
+ mIdPackage = preader.readString();
+ }
+ }
+ }
+
+ if ((flags&FLAGS_HAS_AUTOFILL_DATA) != 0) {
+ mSanitized = in.readInt() == 1;
+ mAutofillId = in.readParcelable(null);
+ mAutofillType = in.readInt();
+ mAutofillHints = in.readStringArray();
+ mAutofillValue = in.readParcelable(null);
+ mAutofillOptions = in.readCharSequenceArray();
+ final Parcelable p = in.readParcelable(null);
+ if (p instanceof HtmlInfo) {
+ mHtmlInfo = (HtmlInfo) p;
+ }
+ }
+ if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
+ mX = in.readInt();
+ mY = in.readInt();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+ } else {
+ int val = in.readInt();
+ mX = val&0x7fff;
+ mY = (val>>16)&0x7fff;
+ val = in.readInt();
+ mWidth = val&0x7fff;
+ mHeight = (val>>16)&0x7fff;
+ }
+ if ((flags&FLAGS_HAS_SCROLL) != 0) {
+ mScrollX = in.readInt();
+ mScrollY = in.readInt();
+ }
+ if ((flags&FLAGS_HAS_MATRIX) != 0) {
+ mMatrix = new Matrix();
+ in.readFloatArray(reader.mTmpMatrix);
+ mMatrix.setValues(reader.mTmpMatrix);
+ }
+ if ((flags&FLAGS_HAS_ELEVATION) != 0) {
+ mElevation = in.readFloat();
+ }
+ if ((flags&FLAGS_HAS_ALPHA) != 0) {
+ mAlpha = in.readFloat();
+ }
+ if ((flags&FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
+ mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+ if ((flags&FLAGS_HAS_TEXT) != 0) {
+ mText = new ViewNodeText(in, (flags&FLAGS_HAS_COMPLEX_TEXT) == 0);
+ }
+ if ((flags&FLAGS_HAS_INPUT_TYPE) != 0) {
+ mInputType = in.readInt();
+ }
+ if ((flags&FLAGS_HAS_URL) != 0) {
+ mWebDomain = in.readString();
+ }
+ if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) {
+ mLocaleList = in.readParcelable(null);
+ }
+ if ((flags&FLAGS_HAS_EXTRAS) != 0) {
+ mExtras = in.readBundle();
+ }
+ if ((flags&FLAGS_HAS_CHILDREN) != 0) {
+ final int NCHILDREN = in.readInt();
+ if (DEBUG_PARCEL_TREE || DEBUG_PARCEL_CHILDREN) Log.d(TAG,
+ "Preparing to read " + NCHILDREN
+ + " children: @ #" + reader.mNumReadViews
+ + ", level " + nestingLevel);
+ mChildren = new ViewNode[NCHILDREN];
+ for (int i=0; i<NCHILDREN; i++) {
+ mChildren[i] = new ViewNode(reader, nestingLevel + 1);
+ }
+ }
+ }
+
+ int writeSelfToParcel(Parcel out, PooledStringWriter pwriter, boolean sanitizeOnWrite,
+ float[] tmpMatrix) {
+ // Guard used to skip non-sanitized data when writing for autofill.
+ boolean writeSensitive = true;
+
+ int flags = mFlags & ~FLAGS_ALL_CONTROL;
+
+ if (mId != View.NO_ID) {
+ flags |= FLAGS_HAS_ID;
+ }
+ if (mAutofillId != null) {
+ flags |= FLAGS_HAS_AUTOFILL_DATA;
+ }
+ if ((mX&~0x7fff) != 0 || (mY&~0x7fff) != 0
+ || (mWidth&~0x7fff) != 0 | (mHeight&~0x7fff) != 0) {
+ flags |= FLAGS_HAS_LARGE_COORDS;
+ }
+ if (mScrollX != 0 || mScrollY != 0) {
+ flags |= FLAGS_HAS_SCROLL;
+ }
+ if (mMatrix != null) {
+ flags |= FLAGS_HAS_MATRIX;
+ }
+ if (mElevation != 0) {
+ flags |= FLAGS_HAS_ELEVATION;
+ }
+ if (mAlpha != 1.0f) {
+ flags |= FLAGS_HAS_ALPHA;
+ }
+ if (mContentDescription != null) {
+ flags |= FLAGS_HAS_CONTENT_DESCRIPTION;
+ }
+ if (mText != null) {
+ flags |= FLAGS_HAS_TEXT;
+ if (!mText.isSimple()) {
+ flags |= FLAGS_HAS_COMPLEX_TEXT;
+ }
+ }
+ if (mInputType != 0) {
+ flags |= FLAGS_HAS_INPUT_TYPE;
+ }
+ if (mWebDomain != null) {
+ flags |= FLAGS_HAS_URL;
+ }
+ if (mLocaleList != null) {
+ flags |= FLAGS_HAS_LOCALE_LIST;
+ }
+ if (mExtras != null) {
+ flags |= FLAGS_HAS_EXTRAS;
+ }
+ if (mChildren != null) {
+ flags |= FLAGS_HAS_CHILDREN;
+ }
+
+ pwriter.writeString(mClassName);
+
+ int writtenFlags = flags;
+ if ((flags&FLAGS_HAS_AUTOFILL_DATA) != 0 && (mSanitized || !sanitizeOnWrite)) {
+ // Remove 'checked' from sanitized autofill request.
+ writtenFlags = flags & ~FLAGS_CHECKED;
+ }
+ if (mAutofillOverlay != null) {
+ if (mAutofillOverlay.focused) {
+ writtenFlags |= ViewNode.FLAGS_FOCUSED;
+ } else {
+ writtenFlags &= ~ViewNode.FLAGS_FOCUSED;
+ }
+ }
+
+ out.writeInt(writtenFlags);
+ if ((flags&FLAGS_HAS_ID) != 0) {
+ out.writeInt(mId);
+ if (mId != 0) {
+ pwriter.writeString(mIdEntry);
+ if (mIdEntry != null) {
+ pwriter.writeString(mIdType);
+ pwriter.writeString(mIdPackage);
+ }
+ }
+ }
+
+ if ((flags&FLAGS_HAS_AUTOFILL_DATA) != 0) {
+ writeSensitive = mSanitized || !sanitizeOnWrite;
+ out.writeInt(mSanitized ? 1 : 0);
+ out.writeParcelable(mAutofillId, 0);
+ out.writeInt(mAutofillType);
+ out.writeStringArray(mAutofillHints);
+ final AutofillValue sanitizedValue;
+ if (writeSensitive) {
+ sanitizedValue = mAutofillValue;
+ } else if (mAutofillOverlay != null && mAutofillOverlay.value != null) {
+ sanitizedValue = mAutofillOverlay.value;
+ } else {
+ sanitizedValue = null;
+ }
+ out.writeParcelable(sanitizedValue, 0);
+ out.writeCharSequenceArray(mAutofillOptions);
+ if (mHtmlInfo instanceof Parcelable) {
+ out.writeParcelable((Parcelable) mHtmlInfo, 0);
+ } else {
+ out.writeParcelable(null, 0);
+ }
+ }
+ if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
+ out.writeInt(mX);
+ out.writeInt(mY);
+ out.writeInt(mWidth);
+ out.writeInt(mHeight);
+ } else {
+ out.writeInt((mY<<16) | mX);
+ out.writeInt((mHeight<<16) | mWidth);
+ }
+ if ((flags&FLAGS_HAS_SCROLL) != 0) {
+ out.writeInt(mScrollX);
+ out.writeInt(mScrollY);
+ }
+ if ((flags&FLAGS_HAS_MATRIX) != 0) {
+ mMatrix.getValues(tmpMatrix);
+ out.writeFloatArray(tmpMatrix);
+ }
+ if ((flags&FLAGS_HAS_ELEVATION) != 0) {
+ out.writeFloat(mElevation);
+ }
+ if ((flags&FLAGS_HAS_ALPHA) != 0) {
+ out.writeFloat(mAlpha);
+ }
+ if ((flags&FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
+ TextUtils.writeToParcel(mContentDescription, out, 0);
+ }
+ if ((flags&FLAGS_HAS_TEXT) != 0) {
+ mText.writeToParcel(out, (flags&FLAGS_HAS_COMPLEX_TEXT) == 0, writeSensitive);
+ }
+ if ((flags&FLAGS_HAS_INPUT_TYPE) != 0) {
+ out.writeInt(mInputType);
+ }
+ if ((flags&FLAGS_HAS_URL) != 0) {
+ out.writeString(mWebDomain);
+ }
+ if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) {
+ out.writeParcelable(mLocaleList, 0);
+ }
+ if ((flags&FLAGS_HAS_EXTRAS) != 0) {
+ out.writeBundle(mExtras);
+ }
+ return flags;
+ }
+
+ /**
+ * Returns the ID associated with this view, as per {@link View#getId() View.getId()}.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * If {@link #getId()} is a resource identifier, this is the package name of that
+ * identifier. See {@link android.view.ViewStructure#setId ViewStructure.setId}
+ * for more information.
+ */
+ public String getIdPackage() {
+ return mIdPackage;
+ }
+
+ /**
+ * If {@link #getId()} is a resource identifier, this is the type name of that
+ * identifier. See {@link android.view.ViewStructure#setId ViewStructure.setId}
+ * for more information.
+ */
+ public String getIdType() {
+ return mIdType;
+ }
+
+ /**
+ * If {@link #getId()} is a resource identifier, this is the entry name of that
+ * identifier. See {@link android.view.ViewStructure#setId ViewStructure.setId}
+ * for more information.
+ */
+ public String getIdEntry() {
+ return mIdEntry;
+ }
+
+ /**
+ * Gets the id that can be used to autofill the view contents.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes.
+ *
+ * @return id that can be used to autofill the view contents, or {@code null} if the
+ * structure was created for assist purposes.
+ */
+ @Nullable public AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ /**
+ * Gets the the type of value that can be used to autofill the view contents.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes.
+ *
+ * @return autofill type as defined by {@link View#getAutofillType()},
+ * or {@link View#AUTOFILL_TYPE_NONE} if the structure was created for assist purposes.
+ */
+ public @View.AutofillType int getAutofillType() {
+ return mAutofillType;
+ }
+
+ /**
+ * Describes the content of a view so that a autofill service can fill in the appropriate
+ * data.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes,
+ * not for Assist - see {@link View#getAutofillHints()} for more info.
+ *
+ * @return The autofill hints for this view, or {@code null} if the structure was created
+ * for assist purposes.
+ */
+ @Nullable public String[] getAutofillHints() {
+ return mAutofillHints;
+ }
+
+ /**
+ * Gets the the value of this view.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes,
+ * not for assist purposes.
+ *
+ * @return the autofill value of this view, or {@code null} if the structure was created
+ * for assist purposes.
+ */
+ @Nullable public AutofillValue getAutofillValue() {
+ return mAutofillValue;
+ }
+
+ /** @hide **/
+ public void setAutofillOverlay(AutofillOverlay overlay) {
+ mAutofillOverlay = overlay;
+ }
+
+ /**
+ * Gets the options that can be used to autofill this view.
+ *
+ * <p>Typically used by nodes whose {@link View#getAutofillType()} is a list to indicate
+ * the meaning of each possible value in the list.
+ *
+ * <p>It's relevant when the {@link AssistStructure} is used for autofill purposes, not
+ * for assist purposes.
+ *
+ * @return the options that can be used to autofill this view, or {@code null} if the
+ * structure was created for assist purposes.
+ */
+ @Nullable public CharSequence[] getAutofillOptions() {
+ return mAutofillOptions;
+ }
+
+ /**
+ * Gets the {@link android.text.InputType} bits of this structure.
+ *
+ * @return bits as defined by {@link android.text.InputType}.
+ */
+ public int getInputType() {
+ return mInputType;
+ }
+
+ /** @hide */
+ public boolean isSanitized() {
+ return mSanitized;
+ }
+
+ /**
+ * Updates the {@link AutofillValue} of this structure.
+ *
+ * <p>Should be used just before sending the structure to the
+ * {@link android.service.autofill.AutofillService} for saving, since it will override the
+ * initial value.
+ *
+ * @hide
+ */
+ public void updateAutofillValue(AutofillValue value) {
+ mAutofillValue = value;
+ if (value.isText()) {
+ if (mText == null) {
+ mText = new ViewNodeText();
+ }
+ mText.mText = value.getTextValue();
+ }
+ }
+
+ /**
+ * Returns the left edge of this view, in pixels, relative to the left edge of its parent.
+ */
+ public int getLeft() {
+ return mX;
+ }
+
+ /**
+ * Returns the top edge of this view, in pixels, relative to the top edge of its parent.
+ */
+ public int getTop() {
+ return mY;
+ }
+
+ /**
+ * Returns the current X scroll offset of this view, as per
+ * {@link android.view.View#getScrollX() View.getScrollX()}.
+ */
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ /**
+ * Returns the current Y scroll offset of this view, as per
+ * {@link android.view.View#getScrollX() View.getScrollY()}.
+ */
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ /**
+ * Returns the width of this view, in pixels.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Returns the height of this view, in pixels.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns the transformation that has been applied to this view, such as a translation
+ * or scaling. The returned Matrix object is owned by ViewNode; do not modify it.
+ * Returns null if there is no transformation applied to the view.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public Matrix getTransformation() {
+ return mMatrix;
+ }
+
+ /**
+ * Returns the visual elevation of the view, used for shadowing and other visual
+ * characterstics, as set by {@link ViewStructure#setElevation
+ * ViewStructure.setElevation(float)}.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public float getElevation() {
+ return mElevation;
+ }
+
+ /**
+ * Returns the alpha transformation of the view, used to reduce the overall opacity
+ * of the view's contents, as set by {@link ViewStructure#setAlpha
+ * ViewStructure.setAlpha(float)}.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Returns the visibility mode of this view, as per
+ * {@link android.view.View#getVisibility() View.getVisibility()}.
+ */
+ public int getVisibility() {
+ return mFlags&ViewNode.FLAGS_VISIBILITY_MASK;
+ }
+
+ /**
+ * Returns true if assist data has been blocked starting at this node in the hierarchy.
+ */
+ public boolean isAssistBlocked() {
+ return (mFlags&ViewNode.FLAGS_ASSIST_BLOCKED) != 0;
+ }
+
+ /**
+ * Returns true if this node is in an enabled state.
+ */
+ public boolean isEnabled() {
+ return (mFlags&ViewNode.FLAGS_DISABLED) == 0;
+ }
+
+ /**
+ * Returns true if this node is clickable by the user.
+ */
+ public boolean isClickable() {
+ return (mFlags&ViewNode.FLAGS_CLICKABLE) != 0;
+ }
+
+ /**
+ * Returns true if this node can take input focus.
+ */
+ public boolean isFocusable() {
+ return (mFlags&ViewNode.FLAGS_FOCUSABLE) != 0;
+ }
+
+ /**
+ * Returns true if this node currently had input focus at the time that the
+ * structure was collected.
+ */
+ public boolean isFocused() {
+ return (mFlags&ViewNode.FLAGS_FOCUSED) != 0;
+ }
+
+ /**
+ * Returns true if this node currently had accessibility focus at the time that the
+ * structure was collected.
+ */
+ public boolean isAccessibilityFocused() {
+ return (mFlags&ViewNode.FLAGS_ACCESSIBILITY_FOCUSED) != 0;
+ }
+
+ /**
+ * Returns true if this node represents something that is checkable by the user.
+ */
+ public boolean isCheckable() {
+ return (mFlags&ViewNode.FLAGS_CHECKABLE) != 0;
+ }
+
+ /**
+ * Returns true if this node is currently in a checked state.
+ */
+ public boolean isChecked() {
+ return (mFlags&ViewNode.FLAGS_CHECKED) != 0;
+ }
+
+ /**
+ * Returns true if this node has currently been selected by the user.
+ */
+ public boolean isSelected() {
+ return (mFlags&ViewNode.FLAGS_SELECTED) != 0;
+ }
+
+ /**
+ * Returns true if this node has currently been activated by the user.
+ */
+ public boolean isActivated() {
+ return (mFlags&ViewNode.FLAGS_ACTIVATED) != 0;
+ }
+
+ /**
+ * Returns true if this node is opaque.
+ */
+ public boolean isOpaque() { return (mFlags&ViewNode.FLAGS_OPAQUE) != 0; }
+
+ /**
+ * Returns true if this node is something the user can perform a long click/press on.
+ */
+ public boolean isLongClickable() {
+ return (mFlags&ViewNode.FLAGS_LONG_CLICKABLE) != 0;
+ }
+
+ /**
+ * Returns true if this node is something the user can perform a context click on.
+ */
+ public boolean isContextClickable() {
+ return (mFlags&ViewNode.FLAGS_CONTEXT_CLICKABLE) != 0;
+ }
+
+ /**
+ * Returns the class name of the node's implementation, indicating its behavior.
+ * For example, a button will report "android.widget.Button" meaning it behaves
+ * like a {@link android.widget.Button}.
+ */
+ public String getClassName() {
+ return mClassName;
+ }
+
+ /**
+ * Returns any content description associated with the node, which semantically describes
+ * its purpose for accessibility and other uses.
+ */
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Returns the domain of the HTML document represented by this view.
+ *
+ * <p>Typically used when the view associated with the view is a container for an HTML
+ * document.
+ *
+ * <strong>WARNING:</strong> a {@link android.service.autofill.AutofillService} should only
+ * use this domain for autofill purposes when it trusts the app generating it (i.e., the app
+ * defined by {@link AssistStructure#getActivityComponent()}).
+ *
+ * @return domain-only part of the document. For example, if the full URL is
+ * {@code http://my.site/login?user=my_user}, it returns {@code my.site}.
+ */
+ @Nullable public String getWebDomain() {
+ return mWebDomain;
+ }
+
+ /**
+ * Returns the HTML properties associated with this view.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes,
+ * not for assist purposes.
+ *
+ * @return the HTML properties associated with this view, or {@code null} if the
+ * structure was created for assist purposes.
+ */
+ @Nullable public HtmlInfo getHtmlInfo() {
+ return mHtmlInfo;
+ }
+
+ /**
+ * Returns the the list of locales associated with this view.
+ */
+ @Nullable public LocaleList getLocaleList() {
+ return mLocaleList;
+ }
+
+ /**
+ * Returns any text associated with the node that is displayed to the user, or null
+ * if there is none.
+ */
+ public CharSequence getText() {
+ return mText != null ? mText.mText : null;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is where the current selection starts.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int getTextSelectionStart() {
+ return mText != null ? mText.mTextSelectionStart : -1;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is where the current selection starts.
+ * If there is no selection, returns the same value as {@link #getTextSelectionStart()},
+ * indicating the cursor position.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int getTextSelectionEnd() {
+ return mText != null ? mText.mTextSelectionEnd : -1;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is the main text color associated with it.
+ * If there is no text color, {@link #TEXT_COLOR_UNDEFINED} is returned.
+ * Note that the text may also contain style spans that modify the color of specific
+ * parts of the text.
+ */
+ public int getTextColor() {
+ return mText != null ? mText.mTextColor : TEXT_COLOR_UNDEFINED;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is the main text background color associated
+ * with it.
+ * If there is no text background color, {@link #TEXT_COLOR_UNDEFINED} is returned.
+ * Note that the text may also contain style spans that modify the color of specific
+ * parts of the text.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int getTextBackgroundColor() {
+ return mText != null ? mText.mTextBackgroundColor : TEXT_COLOR_UNDEFINED;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is the main text size (in pixels) associated
+ * with it.
+ * Note that the text may also contain style spans that modify the size of specific
+ * parts of the text.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public float getTextSize() {
+ return mText != null ? mText.mTextSize : 0;
+ }
+
+ /**
+ * If {@link #getText()} is non-null, this is the main text style associated
+ * with it, containing a bit mask of {@link #TEXT_STYLE_BOLD},
+ * {@link #TEXT_STYLE_BOLD}, {@link #TEXT_STYLE_STRIKE_THRU}, and/or
+ * {@link #TEXT_STYLE_UNDERLINE}.
+ * Note that the text may also contain style spans that modify the style of specific
+ * parts of the text.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int getTextStyle() {
+ return mText != null ? mText.mTextStyle : 0;
+ }
+
+ /**
+ * Return per-line offsets into the text returned by {@link #getText()}. Each entry
+ * in the array is a formatted line of text, and the value it contains is the offset
+ * into the text string where that line starts. May return null if there is no line
+ * information.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int[] getTextLineCharOffsets() {
+ return mText != null ? mText.mLineCharOffsets : null;
+ }
+
+ /**
+ * Return per-line baselines into the text returned by {@link #getText()}. Each entry
+ * in the array is a formatted line of text, and the value it contains is the baseline
+ * where that text appears in the view. May return null if there is no line
+ * information.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for assist purposes,
+ * not for autofill purposes.
+ */
+ public int[] getTextLineBaselines() {
+ return mText != null ? mText.mLineBaselines : null;
+ }
+
+ /**
+ * Return additional hint text associated with the node; this is typically used with
+ * a node that takes user input, describing to the user what the input means.
+ */
+ public String getHint() {
+ return mText != null ? mText.mHint : null;
+ }
+
+ /**
+ * Return a Bundle containing optional vendor-specific extension information.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Return the number of children this node has.
+ */
+ public int getChildCount() {
+ return mChildren != null ? mChildren.length : 0;
+ }
+
+ /**
+ * Return a child of this node, given an index value from 0 to
+ * {@link #getChildCount()}-1.
+ */
+ public ViewNode getChildAt(int index) {
+ return mChildren[index];
+ }
+ }
+
+ /**
+ * POJO used to override some autofill-related values when the node is parcelized.
+ *
+ * @hide
+ */
+ static public class AutofillOverlay {
+ public boolean focused;
+ public AutofillValue value;
+ }
+
+ static class ViewNodeBuilder extends ViewStructure {
+ final AssistStructure mAssist;
+ final ViewNode mNode;
+ final boolean mAsync;
+
+ ViewNodeBuilder(AssistStructure assist, ViewNode node, boolean async) {
+ mAssist = assist;
+ mNode = node;
+ mAsync = async;
+ }
+
+ @Override
+ public void setId(int id, String packageName, String typeName, String entryName) {
+ mNode.mId = id;
+ mNode.mIdPackage = packageName;
+ mNode.mIdType = typeName;
+ mNode.mIdEntry = entryName;
+ }
+
+ @Override
+ public void setDimens(int left, int top, int scrollX, int scrollY, int width, int height) {
+ mNode.mX = left;
+ mNode.mY = top;
+ mNode.mScrollX = scrollX;
+ mNode.mScrollY = scrollY;
+ mNode.mWidth = width;
+ mNode.mHeight = height;
+ }
+
+ @Override
+ public void setTransformation(Matrix matrix) {
+ if (matrix == null) {
+ mNode.mMatrix = null;
+ } else {
+ mNode.mMatrix = new Matrix(matrix);
+ }
+ }
+
+ @Override
+ public void setElevation(float elevation) {
+ mNode.mElevation = elevation;
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ mNode.mAlpha = alpha;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_VISIBILITY_MASK) | visibility;
+ }
+
+ @Override
+ public void setAssistBlocked(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_ASSIST_BLOCKED)
+ | (state ? ViewNode.FLAGS_ASSIST_BLOCKED : 0);
+ }
+
+ @Override
+ public void setEnabled(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_DISABLED)
+ | (state ? 0 : ViewNode.FLAGS_DISABLED);
+ }
+
+ @Override
+ public void setClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_CLICKABLE)
+ | (state ? ViewNode.FLAGS_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setLongClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_LONG_CLICKABLE)
+ | (state ? ViewNode.FLAGS_LONG_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setContextClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_CONTEXT_CLICKABLE)
+ | (state ? ViewNode.FLAGS_CONTEXT_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setFocusable(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_FOCUSABLE)
+ | (state ? ViewNode.FLAGS_FOCUSABLE : 0);
+ }
+
+ @Override
+ public void setFocused(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_FOCUSED)
+ | (state ? ViewNode.FLAGS_FOCUSED : 0);
+ }
+
+ @Override
+ public void setAccessibilityFocused(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_ACCESSIBILITY_FOCUSED)
+ | (state ? ViewNode.FLAGS_ACCESSIBILITY_FOCUSED : 0);
+ }
+
+ @Override
+ public void setCheckable(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_CHECKABLE)
+ | (state ? ViewNode.FLAGS_CHECKABLE : 0);
+ }
+
+ @Override
+ public void setChecked(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_CHECKED)
+ | (state ? ViewNode.FLAGS_CHECKED : 0);
+ }
+
+ @Override
+ public void setSelected(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_SELECTED)
+ | (state ? ViewNode.FLAGS_SELECTED : 0);
+ }
+
+ @Override
+ public void setActivated(boolean state) {
+ mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_ACTIVATED)
+ | (state ? ViewNode.FLAGS_ACTIVATED : 0);
+ }
+
+ @Override
+ public void setOpaque(boolean opaque) {
+ mNode.mFlags = (mNode.mFlags & ~ViewNode.FLAGS_OPAQUE)
+ | (opaque ? ViewNode.FLAGS_OPAQUE : 0);
+ }
+
+ @Override
+ public void setClassName(String className) {
+ mNode.mClassName = className;
+ }
+
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ mNode.mContentDescription = contentDescription;
+ }
+
+ private final ViewNodeText getNodeText() {
+ if (mNode.mText != null) {
+ return mNode.mText;
+ }
+ mNode.mText = new ViewNodeText();
+ return mNode.mText;
+ }
+
+ @Override
+ public void setText(CharSequence text) {
+ ViewNodeText t = getNodeText();
+ t.mText = TextUtils.trimNoCopySpans(text);
+ t.mTextSelectionStart = t.mTextSelectionEnd = -1;
+ }
+
+ @Override
+ public void setText(CharSequence text, int selectionStart, int selectionEnd) {
+ ViewNodeText t = getNodeText();
+ t.mText = TextUtils.trimNoCopySpans(text);
+ t.mTextSelectionStart = selectionStart;
+ t.mTextSelectionEnd = selectionEnd;
+ }
+
+ @Override
+ public void setTextStyle(float size, int fgColor, int bgColor, int style) {
+ ViewNodeText t = getNodeText();
+ t.mTextColor = fgColor;
+ t.mTextBackgroundColor = bgColor;
+ t.mTextSize = size;
+ t.mTextStyle = style;
+ }
+
+ @Override
+ public void setTextLines(int[] charOffsets, int[] baselines) {
+ ViewNodeText t = getNodeText();
+ t.mLineCharOffsets = charOffsets;
+ t.mLineBaselines = baselines;
+ }
+
+ @Override
+ public void setHint(CharSequence hint) {
+ getNodeText().mHint = hint != null ? hint.toString() : null;
+ }
+
+ @Override
+ public CharSequence getText() {
+ return mNode.mText != null ? mNode.mText.mText : null;
+ }
+
+ @Override
+ public int getTextSelectionStart() {
+ return mNode.mText != null ? mNode.mText.mTextSelectionStart : -1;
+ }
+
+ @Override
+ public int getTextSelectionEnd() {
+ return mNode.mText != null ? mNode.mText.mTextSelectionEnd : -1;
+ }
+
+ @Override
+ public CharSequence getHint() {
+ return mNode.mText != null ? mNode.mText.mHint : null;
+ }
+
+ @Override
+ public Bundle getExtras() {
+ if (mNode.mExtras != null) {
+ return mNode.mExtras;
+ }
+ mNode.mExtras = new Bundle();
+ return mNode.mExtras;
+ }
+
+ @Override
+ public boolean hasExtras() {
+ return mNode.mExtras != null;
+ }
+
+ @Override
+ public void setChildCount(int num) {
+ mNode.mChildren = new ViewNode[num];
+ }
+
+ @Override
+ public int addChildCount(int num) {
+ if (mNode.mChildren == null) {
+ setChildCount(num);
+ return 0;
+ }
+ final int start = mNode.mChildren.length;
+ ViewNode[] newArray = new ViewNode[start + num];
+ System.arraycopy(mNode.mChildren, 0, newArray, 0, start);
+ mNode.mChildren = newArray;
+ return start;
+ }
+
+ @Override
+ public int getChildCount() {
+ return mNode.mChildren != null ? mNode.mChildren.length : 0;
+ }
+
+ @Override
+ public ViewStructure newChild(int index) {
+ ViewNode node = new ViewNode();
+ mNode.mChildren[index] = node;
+ return new ViewNodeBuilder(mAssist, node, false);
+ }
+
+ @Override
+ public ViewStructure asyncNewChild(int index) {
+ synchronized (mAssist) {
+ ViewNode node = new ViewNode();
+ mNode.mChildren[index] = node;
+ ViewNodeBuilder builder = new ViewNodeBuilder(mAssist, node, true);
+ mAssist.mPendingAsyncChildren.add(builder);
+ return builder;
+ }
+ }
+
+ @Override
+ public void asyncCommit() {
+ synchronized (mAssist) {
+ if (!mAsync) {
+ throw new IllegalStateException("Child " + this
+ + " was not created with ViewStructure.asyncNewChild");
+ }
+ if (!mAssist.mPendingAsyncChildren.remove(this)) {
+ throw new IllegalStateException("Child " + this + " already committed");
+ }
+ mAssist.notifyAll();
+ }
+ }
+
+ @Override
+ public Rect getTempRect() {
+ return mAssist.mTmpRect;
+ }
+
+ @Override
+ public void setAutofillId(@NonNull AutofillId id) {
+ mNode.mAutofillId = id;
+ }
+
+ @Override
+ public void setAutofillId(@NonNull AutofillId parentId, int virtualId) {
+ mNode.mAutofillId = new AutofillId(parentId, virtualId);
+ }
+
+ @Override
+ public AutofillId getAutofillId() {
+ return mNode.mAutofillId;
+ }
+
+ @Override
+ public void setAutofillType(@View.AutofillType int type) {
+ mNode.mAutofillType = type;
+ }
+
+ @Override
+ public void setAutofillHints(@Nullable String[] hints) {
+ mNode.mAutofillHints = hints;
+ }
+
+ @Override
+ public void setAutofillValue(AutofillValue value) {
+ mNode.mAutofillValue = value;
+ }
+
+ @Override
+ public void setAutofillOptions(CharSequence[] options) {
+ mNode.mAutofillOptions = options;
+ }
+
+ @Override
+ public void setInputType(int inputType) {
+ mNode.mInputType = inputType;
+ }
+
+ @Override
+ public void setDataIsSensitive(boolean sensitive) {
+ mNode.mSanitized = !sensitive;
+ }
+
+ @Override
+ public void setWebDomain(@Nullable String domain) {
+ if (domain == null) {
+ mNode.mWebDomain = null;
+ return;
+ }
+ mNode.mWebDomain = Uri.parse(domain).getHost();
+ }
+
+ @Override
+ public void setLocaleList(LocaleList localeList) {
+ mNode.mLocaleList = localeList;
+ }
+
+ @Override
+ public HtmlInfo.Builder newHtmlInfoBuilder(@NonNull String tagName) {
+ return new HtmlInfoNodeBuilder(tagName);
+ }
+
+ @Override
+ public void setHtmlInfo(@NonNull HtmlInfo htmlInfo) {
+ mNode.mHtmlInfo = htmlInfo;
+ }
+ }
+
+ private static final class HtmlInfoNode extends HtmlInfo implements Parcelable {
+ private final String mTag;
+ private final String[] mNames;
+ private final String[] mValues;
+
+ // Not parcelable
+ private ArrayList<Pair<String, String>> mAttributes;
+
+ private HtmlInfoNode(HtmlInfoNodeBuilder builder) {
+ mTag = builder.mTag;
+ if (builder.mNames == null) {
+ mNames = null;
+ mValues = null;
+ } else {
+ mNames = new String[builder.mNames.size()];
+ mValues = new String[builder.mValues.size()];
+ builder.mNames.toArray(mNames);
+ builder.mValues.toArray(mValues);
+ }
+ }
+
+ @Override
+ public String getTag() {
+ return mTag;
+ }
+
+ @Override
+ public List<Pair<String, String>> getAttributes() {
+ if (mAttributes == null && mNames != null) {
+ mAttributes = new ArrayList<>(mNames.length);
+ for (int i = 0; i < mNames.length; i++) {
+ final Pair<String, String> pair = new Pair<>(mNames[i], mValues[i]);
+ mAttributes.add(i, pair);
+ }
+ }
+ return mAttributes;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mTag);
+ parcel.writeStringArray(mNames);
+ parcel.writeStringArray(mValues);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Creator<HtmlInfoNode> CREATOR = new Creator<HtmlInfoNode>() {
+ @Override
+ public HtmlInfoNode createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final String tag = parcel.readString();
+ final HtmlInfoNodeBuilder builder = new HtmlInfoNodeBuilder(tag);
+ final String[] names = parcel.readStringArray();
+ final String[] values = parcel.readStringArray();
+ if (names != null && values != null) {
+ if (names.length != values.length) {
+ Log.w(TAG, "HtmlInfo attributes mismatch: names=" + names.length
+ + ", values=" + values.length);
+ } else {
+ for (int i = 0; i < names.length; i++) {
+ builder.addAttribute(names[i], values[i]);
+ }
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public HtmlInfoNode[] newArray(int size) {
+ return new HtmlInfoNode[size];
+ }
+ };
+ }
+
+ private static final class HtmlInfoNodeBuilder extends HtmlInfo.Builder {
+ private final String mTag;
+ private ArrayList<String> mNames;
+ private ArrayList<String> mValues;
+
+ HtmlInfoNodeBuilder(String tag) {
+ mTag = tag;
+ }
+
+ @Override
+ public Builder addAttribute(String name, String value) {
+ if (mNames == null) {
+ mNames = new ArrayList<>();
+ mValues = new ArrayList<>();
+ }
+ mNames.add(name);
+ mValues.add(value);
+ return this;
+ }
+
+ @Override
+ public HtmlInfoNode build() {
+ return new HtmlInfoNode(this);
+ }
+ }
+
+ /** @hide */
+ public AssistStructure(Activity activity, boolean forAutoFill, int flags) {
+ mHaveData = true;
+ mActivityComponent = activity.getComponentName();
+ mFlags = flags;
+ ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews(
+ activity.getActivityToken());
+ for (int i=0; i<views.size(); i++) {
+ ViewRootImpl root = views.get(i);
+ if (root.getView() == null) {
+ Log.w(TAG, "Skipping window with dettached view: " + root.getTitle());
+ continue;
+ }
+ mWindowNodes.add(new WindowNode(this, root, forAutoFill, flags));
+ }
+ }
+
+ public AssistStructure() {
+ mHaveData = true;
+ mActivityComponent = null;
+ mFlags = 0;
+ }
+
+ /** @hide */
+ public AssistStructure(Parcel in) {
+ mIsHomeActivity = in.readInt() == 1;
+ mReceiveChannel = in.readStrongBinder();
+ }
+
+ /**
+ * Helper method used to sanitize the structure before it's written to a parcel.
+ *
+ * <p>Used just on autofill.
+ * @hide
+ */
+ public void sanitizeForParceling(boolean sanitize) {
+ mSanitizeOnWrite = sanitize;
+ }
+
+ /** @hide */
+ public void dump(boolean showSensitive) {
+ if (mActivityComponent == null) {
+ Log.i(TAG, "dump(): calling ensureData() first");
+ ensureData();
+ }
+ Log.i(TAG, "Activity: " + mActivityComponent.flattenToShortString());
+ Log.i(TAG, "Sanitize on write: " + mSanitizeOnWrite);
+ Log.i(TAG, "Flags: " + mFlags);
+ final int N = getWindowNodeCount();
+ for (int i=0; i<N; i++) {
+ WindowNode node = getWindowNodeAt(i);
+ Log.i(TAG, "Window #" + i + " [" + node.getLeft() + "," + node.getTop()
+ + " " + node.getWidth() + "x" + node.getHeight() + "]" + " " + node.getTitle());
+ dump(" ", node.getRootViewNode(), showSensitive);
+ }
+ }
+
+ void dump(String prefix, ViewNode node, boolean showSensitive) {
+ Log.i(TAG, prefix + "View [" + node.getLeft() + "," + node.getTop()
+ + " " + node.getWidth() + "x" + node.getHeight() + "]" + " " + node.getClassName());
+ int id = node.getId();
+ if (id != 0) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(prefix); sb.append(" ID: #"); sb.append(Integer.toHexString(id));
+ String entry = node.getIdEntry();
+ if (entry != null) {
+ String type = node.getIdType();
+ String pkg = node.getIdPackage();
+ sb.append(" "); sb.append(pkg); sb.append(":"); sb.append(type);
+ sb.append("/"); sb.append(entry);
+ }
+ Log.i(TAG, sb.toString());
+ }
+ int scrollX = node.getScrollX();
+ int scrollY = node.getScrollY();
+ if (scrollX != 0 || scrollY != 0) {
+ Log.i(TAG, prefix + " Scroll: " + scrollX + "," + scrollY);
+ }
+ Matrix matrix = node.getTransformation();
+ if (matrix != null) {
+ Log.i(TAG, prefix + " Transformation: " + matrix);
+ }
+ float elevation = node.getElevation();
+ if (elevation != 0) {
+ Log.i(TAG, prefix + " Elevation: " + elevation);
+ }
+ float alpha = node.getAlpha();
+ if (alpha != 0) {
+ Log.i(TAG, prefix + " Alpha: " + elevation);
+ }
+ CharSequence contentDescription = node.getContentDescription();
+ if (contentDescription != null) {
+ Log.i(TAG, prefix + " Content description: " + contentDescription);
+ }
+ CharSequence text = node.getText();
+ if (text != null) {
+ final String safeText = node.isSanitized() || showSensitive ? text.toString()
+ : "REDACTED[" + text.length() + " chars]";
+ Log.i(TAG, prefix + " Text (sel " + node.getTextSelectionStart() + "-"
+ + node.getTextSelectionEnd() + "): " + safeText);
+ Log.i(TAG, prefix + " Text size: " + node.getTextSize() + " , style: #"
+ + node.getTextStyle());
+ Log.i(TAG, prefix + " Text color fg: #" + Integer.toHexString(node.getTextColor())
+ + ", bg: #" + Integer.toHexString(node.getTextBackgroundColor()));
+ Log.i(TAG, prefix + " Input type: " + node.getInputType());
+ }
+ String webDomain = node.getWebDomain();
+ if (webDomain != null) {
+ Log.i(TAG, prefix + " Web domain: " + webDomain);
+ }
+ HtmlInfo htmlInfo = node.getHtmlInfo();
+ if (htmlInfo != null) {
+ Log.i(TAG, prefix + " HtmlInfo: tag=" + htmlInfo.getTag()
+ + ", attr="+ htmlInfo.getAttributes());
+ }
+
+ LocaleList localeList = node.getLocaleList();
+ if (localeList != null) {
+ Log.i(TAG, prefix + " LocaleList: " + localeList);
+ }
+ String hint = node.getHint();
+ if (hint != null) {
+ Log.i(TAG, prefix + " Hint: " + hint);
+ }
+ Bundle extras = node.getExtras();
+ if (extras != null) {
+ Log.i(TAG, prefix + " Extras: " + extras);
+ }
+ if (node.isAssistBlocked()) {
+ Log.i(TAG, prefix + " BLOCKED");
+ }
+ AutofillId autofillId = node.getAutofillId();
+ if (autofillId == null) {
+ Log.i(TAG, prefix + " NO autofill ID");
+ } else {
+ Log.i(TAG, prefix + "Autofill info: id= " + autofillId
+ + ", type=" + node.getAutofillType()
+ + ", options=" + Arrays.toString(node.getAutofillOptions())
+ + ", hints=" + Arrays.toString(node.getAutofillHints())
+ + ", value=" + node.getAutofillValue()
+ + ", sanitized=" + node.isSanitized());
+ }
+
+ final int NCHILDREN = node.getChildCount();
+ if (NCHILDREN > 0) {
+ Log.i(TAG, prefix + " Children:");
+ String cprefix = prefix + " ";
+ for (int i=0; i<NCHILDREN; i++) {
+ ViewNode cnode = node.getChildAt(i);
+ dump(cprefix, cnode, showSensitive);
+ }
+ }
+ }
+
+ /**
+ * Return the activity this AssistStructure came from.
+ */
+ public ComponentName getActivityComponent() {
+ ensureData();
+ return mActivityComponent;
+ }
+
+ /** @hide */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether the activity associated with this AssistStructure was the home activity
+ * (Launcher) at the time the assist data was acquired.
+ * @return Whether the activity was the home activity.
+ * @see android.content.Intent#CATEGORY_HOME
+ */
+ public boolean isHomeActivity() {
+ return mIsHomeActivity;
+ }
+
+ /**
+ * Return the number of window contents that have been collected in this assist data.
+ */
+ public int getWindowNodeCount() {
+ ensureData();
+ return mWindowNodes.size();
+ }
+
+ /**
+ * Return one of the windows in the assist data.
+ * @param index Which window to retrieve, may be 0 to {@link #getWindowNodeCount()}-1.
+ */
+ public WindowNode getWindowNodeAt(int index) {
+ ensureData();
+ return mWindowNodes.get(index);
+ }
+
+ /** @hide */
+ public void ensureData() {
+ if (mHaveData) {
+ return;
+ }
+ mHaveData = true;
+ ParcelTransferReader reader = new ParcelTransferReader(mReceiveChannel);
+ reader.go();
+ }
+
+ boolean waitForReady() {
+ boolean skipStructure = false;
+ synchronized (this) {
+ long endTime = SystemClock.uptimeMillis() + 5000;
+ long now;
+ while (mPendingAsyncChildren.size() > 0 && (now=SystemClock.uptimeMillis()) < endTime) {
+ try {
+ wait(endTime-now);
+ } catch (InterruptedException e) {
+ }
+ }
+ if (mPendingAsyncChildren.size() > 0) {
+ // We waited too long, assume none of the assist structure is valid.
+ Log.w(TAG, "Skipping assist structure, waiting too long for async children (have "
+ + mPendingAsyncChildren.size() + " remaining");
+ skipStructure = true;
+ }
+ }
+ return !skipStructure;
+ }
+
+ /** @hide */
+ public void clearSendChannel() {
+ if (mSendChannel != null) {
+ mSendChannel.mAssistStructure = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mIsHomeActivity ? 1 : 0);
+ if (mHaveData) {
+ // This object holds its data. We want to write a send channel that the
+ // other side can use to retrieve that data.
+ if (mSendChannel == null) {
+ mSendChannel = new SendChannel(this);
+ }
+ out.writeStrongBinder(mSendChannel);
+ } else {
+ // This object doesn't hold its data, so just propagate along its receive channel.
+ out.writeStrongBinder(mReceiveChannel);
+ }
+ }
+
+ public static final Parcelable.Creator<AssistStructure> CREATOR
+ = new Parcelable.Creator<AssistStructure>() {
+ @Override
+ public AssistStructure createFromParcel(Parcel in) {
+ return new AssistStructure(in);
+ }
+
+ @Override
+ public AssistStructure[] newArray(int size) {
+ return new AssistStructure[size];
+ }
+ };
+}
diff --git a/android/app/backup/AbsoluteFileBackupHelper.java b/android/app/backup/AbsoluteFileBackupHelper.java
new file mode 100644
index 00000000..a4d99cfd
--- /dev/null
+++ b/android/app/backup/AbsoluteFileBackupHelper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Like FileBackupHelper, but takes absolute paths for the files instead of
+ * subpaths of getFilesDir()
+ *
+ * @hide
+ */
+public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements BackupHelper {
+ private static final String TAG = "AbsoluteFileBackupHelper";
+ private static final boolean DEBUG = false;
+
+ Context mContext;
+ String[] mFiles;
+
+ /**
+ * Construct a helper for backing up / restoring the files at the given absolute locations
+ * within the file system.
+ *
+ * @param context
+ * @param files
+ */
+ public AbsoluteFileBackupHelper(Context context, String... files) {
+ super(context);
+
+ mContext = context;
+ mFiles = files;
+ }
+
+ /**
+ * Based on oldState, determine which of the files from the application's data directory
+ * need to be backed up, write them to the data stream, and fill in newState with the
+ * state as it exists now.
+ */
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) {
+ // use the file paths as the keys, too
+ performBackup_checked(oldState, data, newState, mFiles, mFiles);
+ }
+
+ /**
+ * Restore one absolute file entity from the restore stream
+ */
+ public void restoreEntity(BackupDataInputStream data) {
+ if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size());
+ String key = data.getKey();
+ if (isKeyInList(key, mFiles)) {
+ File f = new File(key);
+ writeFile(f, data);
+ }
+ }
+}
+
diff --git a/android/app/backup/BackupAgent.java b/android/app/backup/BackupAgent.java
new file mode 100644
index 00000000..7aa80d26
--- /dev/null
+++ b/android/app/backup/BackupAgent.java
@@ -0,0 +1,1143 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.app.IBackupAgent;
+import android.app.QueuedWork;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ApplicationInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructStat;
+import android.util.ArraySet;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Provides the central interface between an
+ * application and Android's data backup infrastructure. An application that wishes
+ * to participate in the backup and restore mechanism will declare a subclass of
+ * {@link android.app.backup.BackupAgent}, implement the
+ * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()}
+ * and {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} methods,
+ * and provide the name of its backup agent class in its {@code AndroidManifest.xml} file via
+ * the <code>
+ * <a href="{@docRoot}guide/topics/manifest/application-element.html">&lt;application&gt;</a></code>
+ * tag's {@code android:backupAgent} attribute.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using BackupAgent, read the
+ * <a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a> developer guide.</p></div>
+ *
+ * <h3>Basic Operation</h3>
+ * <p>
+ * When the application makes changes to data that it wishes to keep backed up,
+ * it should call the
+ * {@link android.app.backup.BackupManager#dataChanged() BackupManager.dataChanged()} method.
+ * This notifies the Android Backup Manager that the application needs an opportunity
+ * to update its backup image. The Backup Manager, in turn, schedules a
+ * backup pass to be performed at an opportune time.
+ * <p>
+ * Restore operations are typically performed only when applications are first
+ * installed on a device. At that time, the operating system checks to see whether
+ * there is a previously-saved data set available for the application being installed, and if so,
+ * begins an immediate restore pass to deliver the backup data as part of the installation
+ * process.
+ * <p>
+ * When a backup or restore pass is run, the application's process is launched
+ * (if not already running), the manifest-declared backup agent class (in the {@code
+ * android:backupAgent} attribute) is instantiated within
+ * that process, and the agent's {@link #onCreate()} method is invoked. This prepares the
+ * agent instance to run the actual backup or restore logic. At this point the
+ * agent's
+ * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} or
+ * {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} method will be
+ * invoked as appropriate for the operation being performed.
+ * <p>
+ * A backup data set consists of one or more "entities," flattened binary data
+ * records that are each identified with a key string unique within the data set. Adding a
+ * record to the active data set or updating an existing record is done by simply
+ * writing new entity data under the desired key. Deleting an entity from the data set
+ * is done by writing an entity under that key with header specifying a negative data
+ * size, and no actual entity data.
+ * <p>
+ * <b>Helper Classes</b>
+ * <p>
+ * An extensible agent based on convenient helper classes is available in
+ * {@link android.app.backup.BackupAgentHelper}. That class is particularly
+ * suited to handling of simple file or {@link android.content.SharedPreferences}
+ * backup and restore.
+ * <p>
+ * <b>Threading</b>
+ * <p>
+ * The constructor, as well as {@link #onCreate()} and {@link #onDestroy()} lifecycle callbacks run
+ * on the main thread (UI thread) of the application that implements the BackupAgent.
+ * The data-handling callbacks:
+ * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()},
+ * {@link #onFullBackup(FullBackupDataOutput)},
+ * {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()},
+ * {@link #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) onRestoreFile()},
+ * {@link #onRestoreFinished()}, and {@link #onQuotaExceeded(long, long) onQuotaExceeded()}
+ * run on binder pool threads.
+ *
+ * @see android.app.backup.BackupManager
+ * @see android.app.backup.BackupAgentHelper
+ * @see android.app.backup.BackupDataInput
+ * @see android.app.backup.BackupDataOutput
+ */
+public abstract class BackupAgent extends ContextWrapper {
+ private static final String TAG = "BackupAgent";
+ private static final boolean DEBUG = false;
+
+ /** @hide */
+ public static final int TYPE_EOF = 0;
+
+ /**
+ * During a full restore, indicates that the file system object being restored
+ * is an ordinary file.
+ */
+ public static final int TYPE_FILE = 1;
+
+ /**
+ * During a full restore, indicates that the file system object being restored
+ * is a directory.
+ */
+ public static final int TYPE_DIRECTORY = 2;
+
+ /** @hide */
+ public static final int TYPE_SYMLINK = 3;
+
+ Handler mHandler = null;
+
+ Handler getHandler() {
+ if (mHandler == null) {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+ return mHandler;
+ }
+
+ class SharedPrefsSynchronizer implements Runnable {
+ public final CountDownLatch mLatch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ QueuedWork.waitToFinish();
+ mLatch.countDown();
+ }
+ };
+
+ // Syncing shared preferences deferred writes needs to happen on the main looper thread
+ private void waitForSharedPrefs() {
+ Handler h = getHandler();
+ final SharedPrefsSynchronizer s = new SharedPrefsSynchronizer();
+ h.postAtFrontOfQueue(s);
+ try {
+ s.mLatch.await();
+ } catch (InterruptedException e) { /* ignored */ }
+ }
+
+
+ public BackupAgent() {
+ super(null);
+ }
+
+ /**
+ * Provided as a convenience for agent implementations that need an opportunity
+ * to do one-time initialization before the actual backup or restore operation
+ * is begun.
+ * <p>
+ */
+ public void onCreate() {
+ }
+
+ /**
+ * Provided as a convenience for agent implementations that need to do some
+ * sort of shutdown process after backup or restore is completed.
+ * <p>
+ * Agents do not need to override this method.
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * The application is being asked to write any data changed since the last
+ * time it performed a backup operation. The state data recorded during the
+ * last backup pass is provided in the <code>oldState</code> file
+ * descriptor. If <code>oldState</code> is <code>null</code>, no old state
+ * is available and the application should perform a full backup. In both
+ * cases, a representation of the final backup state after this pass should
+ * be written to the file pointed to by the file descriptor wrapped in
+ * <code>newState</code>.
+ * <p>
+ * Each entity written to the {@link android.app.backup.BackupDataOutput}
+ * <code>data</code> stream will be transmitted
+ * over the current backup transport and stored in the remote data set under
+ * the key supplied as part of the entity. Writing an entity with a negative
+ * data size instructs the transport to delete whatever entity currently exists
+ * under that key from the remote data set.
+ *
+ * @param oldState An open, read-only ParcelFileDescriptor pointing to the
+ * last backup state provided by the application. May be
+ * <code>null</code>, in which case no prior state is being
+ * provided and the application should perform a full backup.
+ * @param data A structured wrapper around an open, read/write
+ * file descriptor pointing to the backup data destination.
+ * Typically the application will use backup helper classes to
+ * write to this file.
+ * @param newState An open, read/write ParcelFileDescriptor pointing to an
+ * empty file. The application should record the final backup
+ * state here after writing the requested data to the <code>data</code>
+ * output stream.
+ */
+ public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException;
+
+ /**
+ * The application is being restored from backup and should replace any
+ * existing data with the contents of the backup. The backup data is
+ * provided through the <code>data</code> parameter. Once
+ * the restore is finished, the application should write a representation of
+ * the final state to the <code>newState</code> file descriptor.
+ * <p>
+ * The application is responsible for properly erasing its old data and
+ * replacing it with the data supplied to this method. No "clear user data"
+ * operation will be performed automatically by the operating system. The
+ * exception to this is in the case of a failed restore attempt: if
+ * onRestore() throws an exception, the OS will assume that the
+ * application's data may now be in an incoherent state, and will clear it
+ * before proceeding.
+ *
+ * @param data A structured wrapper around an open, read-only
+ * file descriptor pointing to a full snapshot of the
+ * application's data. The application should consume every
+ * entity represented in this data stream.
+ * @param appVersionCode The value of the <a
+ * href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code
+ * android:versionCode}</a> manifest attribute,
+ * from the application that backed up this particular data set. This
+ * makes it possible for an application's agent to distinguish among any
+ * possible older data versions when asked to perform the restore
+ * operation.
+ * @param newState An open, read/write ParcelFileDescriptor pointing to an
+ * empty file. The application should record the final backup
+ * state here after restoring its data from the <code>data</code> stream.
+ * When a full-backup dataset is being restored, this will be <code>null</code>.
+ */
+ public abstract void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException;
+
+ /**
+ * The application is having its entire file system contents backed up. {@code data}
+ * points to the backup destination, and the app has the opportunity to choose which
+ * files are to be stored. To commit a file as part of the backup, call the
+ * {@link #fullBackupFile(File, FullBackupDataOutput)} helper method. After all file
+ * data is written to the output, the agent returns from this method and the backup
+ * operation concludes.
+ *
+ * <p>Certain parts of the app's data are never backed up even if the app explicitly
+ * sends them to the output:
+ *
+ * <ul>
+ * <li>The contents of the {@link #getCacheDir()} directory</li>
+ * <li>The contents of the {@link #getCodeCacheDir()} directory</li>
+ * <li>The contents of the {@link #getNoBackupFilesDir()} directory</li>
+ * <li>The contents of the app's shared library directory</li>
+ * </ul>
+ *
+ * <p>The default implementation of this method backs up the entirety of the
+ * application's "owned" file system trees to the output other than the few exceptions
+ * listed above. Apps only need to override this method if they need to impose special
+ * limitations on which files are being stored beyond the control that
+ * {@link #getNoBackupFilesDir()} offers.
+ * Alternatively they can provide an xml resource to specify what data to include or exclude.
+ *
+ *
+ * @param data A structured wrapper pointing to the backup destination.
+ * @throws IOException
+ *
+ * @see Context#getNoBackupFilesDir()
+ * @see ApplicationInfo#fullBackupContent
+ * @see #fullBackupFile(File, FullBackupDataOutput)
+ * @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long)
+ */
+ public void onFullBackup(FullBackupDataOutput data) throws IOException {
+ FullBackup.BackupScheme backupScheme = FullBackup.getBackupScheme(this);
+ if (!backupScheme.isFullBackupContentEnabled()) {
+ return;
+ }
+
+ Map<String, Set<String>> manifestIncludeMap;
+ ArraySet<String> manifestExcludeSet;
+ try {
+ manifestIncludeMap =
+ backupScheme.maybeParseAndGetCanonicalIncludePaths();
+ manifestExcludeSet = backupScheme.maybeParseAndGetCanonicalExcludePaths();
+ } catch (IOException | XmlPullParserException e) {
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "Exception trying to parse fullBackupContent xml file!"
+ + " Aborting full backup.", e);
+ }
+ return;
+ }
+
+ final String packageName = getPackageName();
+ final ApplicationInfo appInfo = getApplicationInfo();
+
+ // System apps have control over where their default storage context
+ // is pointed, so we're always explicit when building paths.
+ final Context ceContext = createCredentialProtectedStorageContext();
+ final String rootDir = ceContext.getDataDir().getCanonicalPath();
+ final String filesDir = ceContext.getFilesDir().getCanonicalPath();
+ final String noBackupDir = ceContext.getNoBackupFilesDir().getCanonicalPath();
+ final String databaseDir = ceContext.getDatabasePath("foo").getParentFile()
+ .getCanonicalPath();
+ final String sharedPrefsDir = ceContext.getSharedPreferencesPath("foo").getParentFile()
+ .getCanonicalPath();
+ final String cacheDir = ceContext.getCacheDir().getCanonicalPath();
+ final String codeCacheDir = ceContext.getCodeCacheDir().getCanonicalPath();
+
+ final Context deContext = createDeviceProtectedStorageContext();
+ final String deviceRootDir = deContext.getDataDir().getCanonicalPath();
+ final String deviceFilesDir = deContext.getFilesDir().getCanonicalPath();
+ final String deviceNoBackupDir = deContext.getNoBackupFilesDir().getCanonicalPath();
+ final String deviceDatabaseDir = deContext.getDatabasePath("foo").getParentFile()
+ .getCanonicalPath();
+ final String deviceSharedPrefsDir = deContext.getSharedPreferencesPath("foo")
+ .getParentFile().getCanonicalPath();
+ final String deviceCacheDir = deContext.getCacheDir().getCanonicalPath();
+ final String deviceCodeCacheDir = deContext.getCodeCacheDir().getCanonicalPath();
+
+ final String libDir = (appInfo.nativeLibraryDir != null)
+ ? new File(appInfo.nativeLibraryDir).getCanonicalPath()
+ : null;
+
+ // Maintain a set of excluded directories so that as we traverse the tree we know we're not
+ // going places we don't expect, and so the manifest includes can't take precedence over
+ // what the framework decides is not to be included.
+ final ArraySet<String> traversalExcludeSet = new ArraySet<String>();
+
+ // Add the directories we always exclude.
+ traversalExcludeSet.add(filesDir);
+ traversalExcludeSet.add(noBackupDir);
+ traversalExcludeSet.add(databaseDir);
+ traversalExcludeSet.add(sharedPrefsDir);
+ traversalExcludeSet.add(cacheDir);
+ traversalExcludeSet.add(codeCacheDir);
+
+ traversalExcludeSet.add(deviceFilesDir);
+ traversalExcludeSet.add(deviceNoBackupDir);
+ traversalExcludeSet.add(deviceDatabaseDir);
+ traversalExcludeSet.add(deviceSharedPrefsDir);
+ traversalExcludeSet.add(deviceCacheDir);
+ traversalExcludeSet.add(deviceCodeCacheDir);
+
+ if (libDir != null) {
+ traversalExcludeSet.add(libDir);
+ }
+
+ // Root dir first.
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(rootDir);
+
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.DEVICE_ROOT_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(deviceRootDir);
+
+ // Data dir next.
+ traversalExcludeSet.remove(filesDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.FILES_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(filesDir);
+
+ traversalExcludeSet.remove(deviceFilesDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.DEVICE_FILES_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(deviceFilesDir);
+
+ // Database directory.
+ traversalExcludeSet.remove(databaseDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.DATABASE_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(databaseDir);
+
+ traversalExcludeSet.remove(deviceDatabaseDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.DEVICE_DATABASE_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(deviceDatabaseDir);
+
+ // SharedPrefs.
+ traversalExcludeSet.remove(sharedPrefsDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(sharedPrefsDir);
+
+ traversalExcludeSet.remove(deviceSharedPrefsDir);
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ traversalExcludeSet.add(deviceSharedPrefsDir);
+
+ // getExternalFilesDir() location associated with this app. Technically there should
+ // not be any files here if the app does not properly have permission to access
+ // external storage, but edge cases happen. fullBackupFileTree() catches
+ // IOExceptions and similar, and treats them as non-fatal, so we rely on that; and
+ // we know a priori that processes running as the system UID are not permitted to
+ // access external storage, so we check for that as well to avoid nastygrams in
+ // the log.
+ if (Process.myUid() != Process.SYSTEM_UID) {
+ File efLocation = getExternalFilesDir(null);
+ if (efLocation != null) {
+ applyXmlFiltersAndDoFullBackupForDomain(
+ packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, manifestIncludeMap,
+ manifestExcludeSet, traversalExcludeSet, data);
+ }
+
+ }
+ }
+
+ /**
+ * Notification that the application's current backup operation causes it to exceed
+ * the maximum size permitted by the transport. The ongoing backup operation is
+ * halted and rolled back: any data that had been stored by a previous backup operation
+ * is still intact. Typically the quota-exceeded state will be detected before any data
+ * is actually transmitted over the network.
+ *
+ * <p>The {@code quotaBytes} value is the total data size currently permitted for this
+ * application. If desired, the application can use this as a hint for determining
+ * how much data to store. For example, a messaging application might choose to
+ * store only the newest messages, dropping enough older content to stay under
+ * the quota.
+ *
+ * <p class="note">Note that the maximum quota for the application can change over
+ * time. In particular, in the future the quota may grow. Applications that adapt
+ * to the quota when deciding what data to store should be aware of this and implement
+ * their data storage mechanisms in a way that can take advantage of additional
+ * quota.
+ *
+ * @param backupDataBytes The amount of data measured while initializing the backup
+ * operation, if the total exceeds the app's alloted quota. If initial measurement
+ * suggested that the data would fit but then too much data was actually submitted
+ * as part of the operation, then this value is the amount of data that had been
+ * streamed into the transport at the time the quota was reached.
+ * @param quotaBytes The maximum data size that the transport currently permits
+ * this application to store as a backup.
+ */
+ public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
+ }
+
+ /**
+ * Check whether the xml yielded any <include/> tag for the provided <code>domainToken</code>.
+ * If so, perform a {@link #fullBackupFileTree} which backs up the file or recurses if the path
+ * is a directory.
+ */
+ private void applyXmlFiltersAndDoFullBackupForDomain(String packageName, String domainToken,
+ Map<String, Set<String>> includeMap,
+ ArraySet<String> filterSet,
+ ArraySet<String> traversalExcludeSet,
+ FullBackupDataOutput data)
+ throws IOException {
+ if (includeMap == null || includeMap.size() == 0) {
+ // Do entire sub-tree for the provided token.
+ fullBackupFileTree(packageName, domainToken,
+ FullBackup.getBackupScheme(this).tokenToDirectoryPath(domainToken),
+ filterSet, traversalExcludeSet, data);
+ } else if (includeMap.get(domainToken) != null) {
+ // This will be null if the xml parsing didn't yield any rules for
+ // this domain (there may still be rules for other domains).
+ for (String includeFile : includeMap.get(domainToken)) {
+ fullBackupFileTree(packageName, domainToken, includeFile, filterSet,
+ traversalExcludeSet, data);
+ }
+ }
+ }
+
+ /**
+ * Write an entire file as part of a full-backup operation. The file's contents
+ * will be delivered to the backup destination along with the metadata necessary
+ * to place it with the proper location and permissions on the device where the
+ * data is restored.
+ *
+ * <p class="note">Attempting to back up files in directories that are ignored by
+ * the backup system will have no effect. For example, if the app calls this method
+ * with a file inside the {@link #getNoBackupFilesDir()} directory, it will be ignored.
+ * See {@link #onFullBackup(FullBackupDataOutput)} for details on what directories
+ * are excluded from backups.
+ *
+ * @param file The file to be backed up. The file must exist and be readable by
+ * the caller.
+ * @param output The destination to which the backed-up file data will be sent.
+ */
+ public final void fullBackupFile(File file, FullBackupDataOutput output) {
+ // Look up where all of our various well-defined dir trees live on this device
+ final String rootDir;
+ final String filesDir;
+ final String nbFilesDir;
+ final String dbDir;
+ final String spDir;
+ final String cacheDir;
+ final String codeCacheDir;
+ final String deviceRootDir;
+ final String deviceFilesDir;
+ final String deviceNbFilesDir;
+ final String deviceDbDir;
+ final String deviceSpDir;
+ final String deviceCacheDir;
+ final String deviceCodeCacheDir;
+ final String libDir;
+
+ String efDir = null;
+ String filePath;
+
+ ApplicationInfo appInfo = getApplicationInfo();
+
+ try {
+ // System apps have control over where their default storage context
+ // is pointed, so we're always explicit when building paths.
+ final Context ceContext = createCredentialProtectedStorageContext();
+ rootDir = ceContext.getDataDir().getCanonicalPath();
+ filesDir = ceContext.getFilesDir().getCanonicalPath();
+ nbFilesDir = ceContext.getNoBackupFilesDir().getCanonicalPath();
+ dbDir = ceContext.getDatabasePath("foo").getParentFile().getCanonicalPath();
+ spDir = ceContext.getSharedPreferencesPath("foo").getParentFile().getCanonicalPath();
+ cacheDir = ceContext.getCacheDir().getCanonicalPath();
+ codeCacheDir = ceContext.getCodeCacheDir().getCanonicalPath();
+
+ final Context deContext = createDeviceProtectedStorageContext();
+ deviceRootDir = deContext.getDataDir().getCanonicalPath();
+ deviceFilesDir = deContext.getFilesDir().getCanonicalPath();
+ deviceNbFilesDir = deContext.getNoBackupFilesDir().getCanonicalPath();
+ deviceDbDir = deContext.getDatabasePath("foo").getParentFile().getCanonicalPath();
+ deviceSpDir = deContext.getSharedPreferencesPath("foo").getParentFile()
+ .getCanonicalPath();
+ deviceCacheDir = deContext.getCacheDir().getCanonicalPath();
+ deviceCodeCacheDir = deContext.getCodeCacheDir().getCanonicalPath();
+
+ libDir = (appInfo.nativeLibraryDir == null)
+ ? null
+ : new File(appInfo.nativeLibraryDir).getCanonicalPath();
+
+ // may or may not have external files access to attempt backup/restore there
+ if (Process.myUid() != Process.SYSTEM_UID) {
+ File efLocation = getExternalFilesDir(null);
+ if (efLocation != null) {
+ efDir = efLocation.getCanonicalPath();
+ }
+ }
+
+ // Now figure out which well-defined tree the file is placed in, working from
+ // most to least specific. We also specifically exclude the lib, cache,
+ // and code_cache dirs.
+ filePath = file.getCanonicalPath();
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to obtain canonical paths");
+ return;
+ }
+
+ if (filePath.startsWith(cacheDir)
+ || filePath.startsWith(codeCacheDir)
+ || filePath.startsWith(nbFilesDir)
+ || filePath.startsWith(deviceCacheDir)
+ || filePath.startsWith(deviceCodeCacheDir)
+ || filePath.startsWith(deviceNbFilesDir)
+ || filePath.startsWith(libDir)) {
+ Log.w(TAG, "lib, cache, code_cache, and no_backup files are not backed up");
+ return;
+ }
+
+ final String domain;
+ String rootpath = null;
+ if (filePath.startsWith(dbDir)) {
+ domain = FullBackup.DATABASE_TREE_TOKEN;
+ rootpath = dbDir;
+ } else if (filePath.startsWith(spDir)) {
+ domain = FullBackup.SHAREDPREFS_TREE_TOKEN;
+ rootpath = spDir;
+ } else if (filePath.startsWith(filesDir)) {
+ domain = FullBackup.FILES_TREE_TOKEN;
+ rootpath = filesDir;
+ } else if (filePath.startsWith(rootDir)) {
+ domain = FullBackup.ROOT_TREE_TOKEN;
+ rootpath = rootDir;
+ } else if (filePath.startsWith(deviceDbDir)) {
+ domain = FullBackup.DEVICE_DATABASE_TREE_TOKEN;
+ rootpath = deviceDbDir;
+ } else if (filePath.startsWith(deviceSpDir)) {
+ domain = FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
+ rootpath = deviceSpDir;
+ } else if (filePath.startsWith(deviceFilesDir)) {
+ domain = FullBackup.DEVICE_FILES_TREE_TOKEN;
+ rootpath = deviceFilesDir;
+ } else if (filePath.startsWith(deviceRootDir)) {
+ domain = FullBackup.DEVICE_ROOT_TREE_TOKEN;
+ rootpath = deviceRootDir;
+ } else if ((efDir != null) && filePath.startsWith(efDir)) {
+ domain = FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
+ rootpath = efDir;
+ } else {
+ Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping");
+ return;
+ }
+
+ // And now that we know where it lives, semantically, back it up appropriately
+ // In the measurement case, backupToTar() updates the size in output and returns
+ // without transmitting any file data.
+ if (DEBUG) Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain
+ + " rootpath=" + rootpath);
+
+ FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, output);
+ }
+
+ /**
+ * Scan the dir tree (if it actually exists) and process each entry we find. If the
+ * 'excludes' parameters are non-null, they are consulted each time a new file system entity
+ * is visited to see whether that entity (and its subtree, if appropriate) should be
+ * omitted from the backup process.
+ *
+ * @param systemExcludes An optional list of excludes.
+ * @hide
+ */
+ protected final void fullBackupFileTree(String packageName, String domain, String startingPath,
+ ArraySet<String> manifestExcludes,
+ ArraySet<String> systemExcludes,
+ FullBackupDataOutput output) {
+ // Pull out the domain and set it aside to use when making the tarball.
+ String domainPath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain);
+ if (domainPath == null) {
+ // Should never happen.
+ return;
+ }
+
+ File rootFile = new File(startingPath);
+ if (rootFile.exists()) {
+ LinkedList<File> scanQueue = new LinkedList<File>();
+ scanQueue.add(rootFile);
+
+ while (scanQueue.size() > 0) {
+ File file = scanQueue.remove(0);
+ String filePath;
+ try {
+ // Ignore things that aren't "real" files or dirs
+ StructStat stat = Os.lstat(file.getPath());
+ if (!OsConstants.S_ISREG(stat.st_mode)
+ && !OsConstants.S_ISDIR(stat.st_mode)) {
+ if (DEBUG) Log.i(TAG, "Not a file/dir (skipping)!: " + file);
+ continue;
+ }
+
+ // For all other verification, look at the canonicalized path
+ filePath = file.getCanonicalPath();
+
+ // prune this subtree?
+ if (manifestExcludes != null && manifestExcludes.contains(filePath)) {
+ continue;
+ }
+ if (systemExcludes != null && systemExcludes.contains(filePath)) {
+ continue;
+ }
+
+ // If it's a directory, enqueue its contents for scanning.
+ if (OsConstants.S_ISDIR(stat.st_mode)) {
+ File[] contents = file.listFiles();
+ if (contents != null) {
+ for (File entry : contents) {
+ scanQueue.add(0, entry);
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file);
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER, "Error canonicalizing path of " + file);
+ }
+ continue;
+ } catch (ErrnoException e) {
+ if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e);
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER, "Error scanning file " + file + " : " + e);
+ }
+ continue;
+ }
+
+ // Finally, back this file up (or measure it) before proceeding
+ FullBackup.backupToTar(packageName, domain, null, domainPath, filePath, output);
+ }
+ }
+ }
+
+ /**
+ * Handle the data delivered via the given file descriptor during a full restore
+ * operation. The agent is given the path to the file's original location as well
+ * as its size and metadata.
+ * <p>
+ * The file descriptor can only be read for {@code size} bytes; attempting to read
+ * more data has undefined behavior.
+ * <p>
+ * The default implementation creates the destination file/directory and populates it
+ * with the data from the file descriptor, then sets the file's access mode and
+ * modification time to match the restore arguments.
+ *
+ * @param data A read-only file descriptor from which the agent can read {@code size}
+ * bytes of file data.
+ * @param size The number of bytes of file content to be restored to the given
+ * destination. If the file system object being restored is a directory, {@code size}
+ * will be zero.
+ * @param destination The File on disk to be restored with the given data.
+ * @param type The kind of file system object being restored. This will be either
+ * {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}.
+ * @param mode The access mode to be assigned to the destination after its data is
+ * written. This is in the standard format used by {@code chmod()}.
+ * @param mtime The modification time of the file when it was backed up, suitable to
+ * be assigned to the file after its data is written.
+ * @throws IOException
+ */
+ public void onRestoreFile(ParcelFileDescriptor data, long size,
+ File destination, int type, long mode, long mtime)
+ throws IOException {
+
+ final boolean accept = isFileEligibleForRestore(destination);
+ // If we don't accept the file, consume the bytes from the pipe anyway.
+ FullBackup.restoreFile(data, size, type, mode, mtime, accept ? destination : null);
+ }
+
+ private boolean isFileEligibleForRestore(File destination) throws IOException {
+ FullBackup.BackupScheme bs = FullBackup.getBackupScheme(this);
+ if (!bs.isFullBackupContentEnabled()) {
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "onRestoreFile \"" + destination.getCanonicalPath()
+ + "\" : fullBackupContent not enabled for " + getPackageName());
+ }
+ return false;
+ }
+
+ Map<String, Set<String>> includes = null;
+ ArraySet<String> excludes = null;
+ final String destinationCanonicalPath = destination.getCanonicalPath();
+ try {
+ includes = bs.maybeParseAndGetCanonicalIncludePaths();
+ excludes = bs.maybeParseAndGetCanonicalExcludePaths();
+ } catch (XmlPullParserException e) {
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "onRestoreFile \"" + destinationCanonicalPath
+ + "\" : Exception trying to parse fullBackupContent xml file!"
+ + " Aborting onRestoreFile.", e);
+ }
+ return false;
+ }
+
+ if (excludes != null &&
+ isFileSpecifiedInPathList(destination, excludes)) {
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "onRestoreFile: \"" + destinationCanonicalPath + "\": listed in"
+ + " excludes; skipping.");
+ }
+ return false;
+ }
+
+ if (includes != null && !includes.isEmpty()) {
+ // Rather than figure out the <include/> domain based on the path (a lot of code, and
+ // it's a small list), we'll go through and look for it.
+ boolean explicitlyIncluded = false;
+ for (Set<String> domainIncludes : includes.values()) {
+ explicitlyIncluded |= isFileSpecifiedInPathList(destination, domainIncludes);
+ if (explicitlyIncluded) {
+ break;
+ }
+ }
+ if (!explicitlyIncluded) {
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "onRestoreFile: Trying to restore \""
+ + destinationCanonicalPath + "\" but it isn't specified"
+ + " in the included files; skipping.");
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return True if the provided file is either directly in the provided list, or the provided
+ * file is within a directory in the list.
+ */
+ private boolean isFileSpecifiedInPathList(File file, Collection<String> canonicalPathList)
+ throws IOException {
+ for (String canonicalPath : canonicalPathList) {
+ File fileFromList = new File(canonicalPath);
+ if (fileFromList.isDirectory()) {
+ if (file.isDirectory()) {
+ // If they are both directories check exact equals.
+ return file.equals(fileFromList);
+ } else {
+ // O/w we have to check if the file is within the directory from the list.
+ return file.getCanonicalPath().startsWith(canonicalPath);
+ }
+ } else {
+ if (file.equals(fileFromList)) {
+ // Need to check the explicit "equals" so we don't end up with substrings.
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Only specialized platform agents should overload this entry point to support
+ * restores to crazy non-app locations.
+ * @hide
+ */
+ protected void onRestoreFile(ParcelFileDescriptor data, long size,
+ int type, String domain, String path, long mode, long mtime)
+ throws IOException {
+ String basePath = null;
+
+ if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type
+ + " domain=" + domain + " relpath=" + path + " mode=" + mode
+ + " mtime=" + mtime);
+
+ basePath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain);
+ if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
+ mode = -1; // < 0 is a token to skip attempting a chmod()
+ }
+
+ // Now that we've figured out where the data goes, send it on its way
+ if (basePath != null) {
+ // Canonicalize the nominal path and verify that it lies within the stated domain
+ File outFile = new File(basePath, path);
+ String outPath = outFile.getCanonicalPath();
+ if (outPath.startsWith(basePath + File.separatorChar)) {
+ if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outPath);
+ onRestoreFile(data, size, outFile, type, mode, mtime);
+ return;
+ } else {
+ // Attempt to restore to a path outside the file's nominal domain.
+ if (DEBUG) {
+ Log.e(TAG, "Cross-domain restore attempt: " + outPath);
+ }
+ }
+ }
+
+ // Not a supported output location, or bad path: we need to consume the data
+ // anyway, so just use the default "copy the data out" implementation
+ // with a null destination.
+ if (DEBUG) Log.i(TAG, "[ skipping file " + path + "]");
+ FullBackup.restoreFile(data, size, type, mode, mtime, null);
+ }
+
+ /**
+ * The application's restore operation has completed. This method is called after
+ * all available data has been delivered to the application for restore (via either
+ * the {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} or
+ * {@link #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) onRestoreFile()}
+ * callbacks). This provides the app with a stable end-of-restore opportunity to
+ * perform any appropriate post-processing on the data that was just delivered.
+ *
+ * @see #onRestore(BackupDataInput, int, ParcelFileDescriptor)
+ * @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long)
+ */
+ public void onRestoreFinished() {
+ }
+
+ // ----- Core implementation -----
+
+ /** @hide */
+ public final IBinder onBind() {
+ return mBinder;
+ }
+
+ private final IBinder mBinder = new BackupServiceBinder().asBinder();
+
+ /** @hide */
+ public void attach(Context context) {
+ attachBaseContext(context);
+ }
+
+ // ----- IBackupService binder interface -----
+ private class BackupServiceBinder extends IBackupAgent.Stub {
+ private static final String TAG = "BackupServiceBinder";
+
+ @Override
+ public void doBackup(ParcelFileDescriptor oldState,
+ ParcelFileDescriptor data,
+ ParcelFileDescriptor newState,
+ long quotaBytes, int token, IBackupManager callbackBinder) throws RemoteException {
+ // Ensure that we're running with the app's normal permission level
+ long ident = Binder.clearCallingIdentity();
+
+ if (DEBUG) Log.v(TAG, "doBackup() invoked");
+ BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor(), quotaBytes);
+
+ try {
+ BackupAgent.this.onBackup(oldState, output, newState);
+ } catch (IOException ex) {
+ Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw new RuntimeException(ex);
+ } catch (RuntimeException ex) {
+ Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw ex;
+ } finally {
+ // Ensure that any SharedPreferences writes have landed after the backup,
+ // in case the app code has side effects (since apps cannot provide this
+ // guarantee themselves).
+ waitForSharedPrefs();
+
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, 0);
+ } catch (RemoteException e) {
+ // we'll time out anyway, so we're safe
+ }
+
+ // Don't close the fd out from under the system service if this was local
+ if (Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(oldState);
+ IoUtils.closeQuietly(data);
+ IoUtils.closeQuietly(newState);
+ }
+ }
+ }
+
+ @Override
+ public void doRestore(ParcelFileDescriptor data, int appVersionCode,
+ ParcelFileDescriptor newState,
+ int token, IBackupManager callbackBinder) throws RemoteException {
+ // Ensure that we're running with the app's normal permission level
+ long ident = Binder.clearCallingIdentity();
+
+ if (DEBUG) Log.v(TAG, "doRestore() invoked");
+
+ // Ensure that any side-effect SharedPreferences writes have landed *before*
+ // we may be about to rewrite the file out from underneath
+ waitForSharedPrefs();
+
+ BackupDataInput input = new BackupDataInput(data.getFileDescriptor());
+ try {
+ BackupAgent.this.onRestore(input, appVersionCode, newState);
+ } catch (IOException ex) {
+ Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw new RuntimeException(ex);
+ } catch (RuntimeException ex) {
+ Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw ex;
+ } finally {
+ // And bring live SharedPreferences instances up to date
+ reloadSharedPreferences();
+
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, 0);
+ } catch (RemoteException e) {
+ // we'll time out anyway, so we're safe
+ }
+
+ if (Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(data);
+ IoUtils.closeQuietly(newState);
+ }
+ }
+ }
+
+ @Override
+ public void doFullBackup(ParcelFileDescriptor data,
+ long quotaBytes, int token, IBackupManager callbackBinder) {
+ // Ensure that we're running with the app's normal permission level
+ long ident = Binder.clearCallingIdentity();
+
+ if (DEBUG) Log.v(TAG, "doFullBackup() invoked");
+
+ // Ensure that any SharedPreferences writes have landed *before*
+ // we potentially try to back up the underlying files directly.
+ waitForSharedPrefs();
+
+ try {
+ BackupAgent.this.onFullBackup(new FullBackupDataOutput(data, quotaBytes));
+ } catch (IOException ex) {
+ Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw new RuntimeException(ex);
+ } catch (RuntimeException ex) {
+ Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw ex;
+ } finally {
+ // ... and then again after, as in the doBackup() case
+ waitForSharedPrefs();
+
+ // Send the EOD marker indicating that there is no more data
+ // forthcoming from this agent.
+ try {
+ FileOutputStream out = new FileOutputStream(data.getFileDescriptor());
+ byte[] buf = new byte[4];
+ out.write(buf);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to finalize backup stream!");
+ }
+
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, 0);
+ } catch (RemoteException e) {
+ // we'll time out anyway, so we're safe
+ }
+
+ if (Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(data);
+ }
+ }
+ }
+
+ public void doMeasureFullBackup(long quotaBytes, int token, IBackupManager callbackBinder) {
+ // Ensure that we're running with the app's normal permission level
+ final long ident = Binder.clearCallingIdentity();
+ FullBackupDataOutput measureOutput = new FullBackupDataOutput(quotaBytes);
+
+ waitForSharedPrefs();
+ try {
+ BackupAgent.this.onFullBackup(measureOutput);
+ } catch (IOException ex) {
+ Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw new RuntimeException(ex);
+ } catch (RuntimeException ex) {
+ Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex);
+ throw ex;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, measureOutput.getSize());
+ } catch (RemoteException e) {
+ // timeout, so we're safe
+ }
+ }
+ }
+
+ @Override
+ public void doRestoreFile(ParcelFileDescriptor data, long size,
+ int type, String domain, String path, long mode, long mtime,
+ int token, IBackupManager callbackBinder) throws RemoteException {
+ long ident = Binder.clearCallingIdentity();
+ try {
+ BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime);
+ } catch (IOException e) {
+ Log.d(TAG, "onRestoreFile (" + BackupAgent.this.getClass().getName() + ") threw", e);
+ throw new RuntimeException(e);
+ } finally {
+ // Ensure that any side-effect SharedPreferences writes have landed
+ waitForSharedPrefs();
+ // And bring live SharedPreferences instances up to date
+ reloadSharedPreferences();
+
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, 0);
+ } catch (RemoteException e) {
+ // we'll time out anyway, so we're safe
+ }
+
+ if (Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(data);
+ }
+ }
+ }
+
+ @Override
+ public void doRestoreFinished(int token, IBackupManager callbackBinder) {
+ long ident = Binder.clearCallingIdentity();
+ try {
+ BackupAgent.this.onRestoreFinished();
+ } catch (Exception e) {
+ Log.d(TAG, "onRestoreFinished (" + BackupAgent.this.getClass().getName() + ") threw", e);
+ throw e;
+ } finally {
+ // Ensure that any side-effect SharedPreferences writes have landed
+ waitForSharedPrefs();
+
+ Binder.restoreCallingIdentity(ident);
+ try {
+ callbackBinder.opComplete(token, 0);
+ } catch (RemoteException e) {
+ // we'll time out anyway, so we're safe
+ }
+ }
+ }
+
+ @Override
+ public void fail(String message) {
+ getHandler().post(new FailRunnable(message));
+ }
+
+ @Override
+ public void doQuotaExceeded(long backupDataBytes, long quotaBytes) {
+ long ident = Binder.clearCallingIdentity();
+ try {
+ BackupAgent.this.onQuotaExceeded(backupDataBytes, quotaBytes);
+ } catch (Exception e) {
+ Log.d(TAG, "onQuotaExceeded(" + BackupAgent.this.getClass().getName() + ") threw",
+ e);
+ throw e;
+ } finally {
+ waitForSharedPrefs();
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ static class FailRunnable implements Runnable {
+ private String mMessage;
+
+ FailRunnable(String message) {
+ mMessage = message;
+ }
+
+ @Override
+ public void run() {
+ throw new IllegalStateException(mMessage);
+ }
+ }
+}
diff --git a/android/app/backup/BackupAgentHelper.java b/android/app/backup/BackupAgentHelper.java
new file mode 100644
index 00000000..45daead1
--- /dev/null
+++ b/android/app/backup/BackupAgentHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/**
+ * A convenient {@link BackupAgent} wrapper class that automatically manages
+ * heterogeneous data sets within the backup data, each identified by a unique
+ * key prefix. When processing a backup or restore operation, the BackupAgentHelper
+ * dispatches to one or more installed {@link BackupHelper} objects, each
+ * of which is responsible for a defined subset of the data being processed.
+ * <p>
+ * An application will typically extend this class in its own
+ * backup agent. Then, within the agent's {@link BackupAgent#onCreate() onCreate()}
+ * method, it will call {@link #addHelper(String, BackupHelper) addHelper()} one or more times to
+ * install the handlers for each kind of data it wishes to manage within its backups.
+ * <p>
+ * The Android framework currently provides two predefined {@link BackupHelper} classes:</p>
+ * <ul><li>{@link FileBackupHelper} - Manages the backup and restore of entire files
+ * within an application's data directory hierarchy.</li>
+ * <li>{@link SharedPreferencesBackupHelper} - Manages the backup and restore of an
+ * application's {@link android.content.SharedPreferences} data.</li></ul>
+ * <p>
+ * An application can also implement its own helper classes to work within the
+ * {@link BackupAgentHelper} framework. See the {@link BackupHelper} interface
+ * documentation for details.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using BackupAgentHelper, read the
+ * <a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a> developer guide.</p>
+ * </div>
+ *
+ * @see BackupHelper
+ * @see FileBackupHelper
+ * @see SharedPreferencesBackupHelper
+ */
+public class BackupAgentHelper extends BackupAgent {
+ static final String TAG = "BackupAgentHelper";
+
+ BackupHelperDispatcher mDispatcher = new BackupHelperDispatcher();
+
+ /**
+ * Run the backup process on each of the configured handlers.
+ */
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ mDispatcher.performBackup(oldState, data, newState);
+ }
+
+ /**
+ * Run the restore process on each of the configured handlers.
+ */
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ mDispatcher.performRestore(data, appVersionCode, newState);
+ }
+
+ /** @hide */
+ public BackupHelperDispatcher getDispatcher() {
+ return mDispatcher;
+ }
+
+ /**
+ * Add a helper for a given data subset to the agent's configuration. Each helper
+ * must have a prefix string that is unique within this backup agent's set of
+ * helpers.
+ *
+ * @param keyPrefix A string used to disambiguate the various helpers within this agent
+ * @param helper A backup/restore helper object to be invoked during backup and restore
+ * operations.
+ */
+ public void addHelper(String keyPrefix, BackupHelper helper) {
+ mDispatcher.addHelper(keyPrefix, helper);
+ }
+}
+
+
diff --git a/android/app/backup/BackupDataInput.java b/android/app/backup/BackupDataInput.java
new file mode 100644
index 00000000..26f9e3ef
--- /dev/null
+++ b/android/app/backup/BackupDataInput.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides the structured interface through which a {@link BackupAgent} reads
+ * information from the backup data set, via its
+ * {@link BackupAgent#onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor) onRestore()}
+ * method. The data is presented as a set of "entities," each
+ * representing one named record as previously stored by the agent's
+ * {@link BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor)
+ * onBackup()} implementation. An entity is composed of a descriptive header plus a
+ * byte array that holds the raw data saved in the remote backup.
+ * <p>
+ * The agent must consume every entity in the data stream, otherwise the
+ * restored state of the application will be incomplete.
+ * <h3>Example</h3>
+ * <p>
+ * A typical
+ * {@link BackupAgent#onRestore(BackupDataInput,int,ParcelFileDescriptor)
+ * onRestore()} implementation might be structured something like this:
+ * <pre>
+ * public void onRestore(BackupDataInput data, int appVersionCode,
+ * ParcelFileDescriptor newState) {
+ * while (data.readNextHeader()) {
+ * String key = data.getKey();
+ * int dataSize = data.getDataSize();
+ *
+ * if (key.equals(MY_BACKUP_KEY_ONE)) {
+ * // process this kind of record here
+ * byte[] buffer = new byte[dataSize];
+ * data.readEntityData(buffer, 0, dataSize); // reads the entire entity at once
+ *
+ * // now 'buffer' holds the raw data and can be processed however
+ * // the agent wishes
+ * processBackupKeyOne(buffer);
+ * } else if (key.equals(MY_BACKUP_KEY_TO_IGNORE) {
+ * // a key we recognize but wish to discard
+ * data.skipEntityData();
+ * } // ... etc.
+ * }
+ * }</pre>
+ */
+public class BackupDataInput {
+ long mBackupReader;
+
+ private EntityHeader mHeader = new EntityHeader();
+ private boolean mHeaderReady;
+
+ private static class EntityHeader {
+ String key;
+ int dataSize;
+ }
+
+ /** @hide */
+ @SystemApi
+ public BackupDataInput(FileDescriptor fd) {
+ if (fd == null) throw new NullPointerException();
+ mBackupReader = ctor(fd);
+ if (mBackupReader == 0) {
+ throw new RuntimeException("Native initialization failed with fd=" + fd);
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dtor(mBackupReader);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Extract the next entity header from the restore stream. After this method
+ * return success, the {@link #getKey()} and {@link #getDataSize()} methods can
+ * be used to inspect the entity that is now available for processing.
+ *
+ * @return <code>true</code> when there is an entity ready for consumption from the
+ * restore stream, <code>false</code> if the restore stream has been fully consumed.
+ * @throws IOException if an error occurred while reading the restore stream
+ */
+ public boolean readNextHeader() throws IOException {
+ int result = readNextHeader_native(mBackupReader, mHeader);
+ if (result == 0) {
+ // read successfully
+ mHeaderReady = true;
+ return true;
+ } else if (result > 0) {
+ // done
+ mHeaderReady = false;
+ return false;
+ } else {
+ // error
+ mHeaderReady = false;
+ throw new IOException("failed: 0x" + Integer.toHexString(result));
+ }
+ }
+
+ /**
+ * Report the key associated with the current entity in the restore stream
+ * @return the current entity's key string
+ * @throws IllegalStateException if the next record header has not yet been read
+ */
+ public String getKey() {
+ if (mHeaderReady) {
+ return mHeader.key;
+ } else {
+ throw new IllegalStateException("Entity header not read");
+ }
+ }
+
+ /**
+ * Report the size in bytes of the data associated with the current entity in the
+ * restore stream.
+ *
+ * @return The size of the record's raw data, in bytes
+ * @throws IllegalStateException if the next record header has not yet been read
+ */
+ public int getDataSize() {
+ if (mHeaderReady) {
+ return mHeader.dataSize;
+ } else {
+ throw new IllegalStateException("Entity header not read");
+ }
+ }
+
+ /**
+ * Read a record's raw data from the restore stream. The record's header must first
+ * have been processed by the {@link #readNextHeader()} method. Multiple calls to
+ * this method may be made in order to process the data in chunks; not all of it
+ * must be read in a single call. Once all of the raw data for the current entity
+ * has been read, further calls to this method will simply return zero.
+ *
+ * @param data An allocated byte array of at least 'size' bytes
+ * @param offset Offset within the 'data' array at which the data will be placed
+ * when read from the stream
+ * @param size The number of bytes to read in this pass
+ * @return The number of bytes of data read. Once all of the data for this entity
+ * has been read, further calls to this method will return zero.
+ * @throws IOException if an error occurred when trying to read the restore data stream
+ */
+ public int readEntityData(byte[] data, int offset, int size) throws IOException {
+ if (mHeaderReady) {
+ int result = readEntityData_native(mBackupReader, data, offset, size);
+ if (result >= 0) {
+ return result;
+ } else {
+ throw new IOException("result=0x" + Integer.toHexString(result));
+ }
+ } else {
+ throw new IllegalStateException("Entity header not read");
+ }
+ }
+
+ /**
+ * Consume the current entity's data without extracting it into a buffer
+ * for further processing. This allows a {@link android.app.backup.BackupAgent} to
+ * efficiently discard obsolete or otherwise uninteresting records during the
+ * restore operation.
+ *
+ * @throws IOException if an error occurred when trying to read the restore data stream
+ */
+ public void skipEntityData() throws IOException {
+ if (mHeaderReady) {
+ skipEntityData_native(mBackupReader);
+ } else {
+ throw new IllegalStateException("Entity header not read");
+ }
+ }
+
+ private native static long ctor(FileDescriptor fd);
+ private native static void dtor(long mBackupReader);
+
+ private native int readNextHeader_native(long mBackupReader, EntityHeader entity);
+ private native int readEntityData_native(long mBackupReader, byte[] data, int offset, int size);
+ private native int skipEntityData_native(long mBackupReader);
+}
diff --git a/android/app/backup/BackupDataInputStream.java b/android/app/backup/BackupDataInputStream.java
new file mode 100644
index 00000000..94c78457
--- /dev/null
+++ b/android/app/backup/BackupDataInputStream.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * Provides an {@link java.io.InputStream}-like interface for accessing an
+ * entity's data during a restore operation. Used by {@link BackupHelper} classes within the {@link
+ * BackupAgentHelper} mechanism.
+ * <p>
+ * When {@link BackupHelper#restoreEntity(BackupDataInputStream) BackupHelper.restoreEntity()}
+ * is called, the current entity's header has already been read from the underlying
+ * {@link BackupDataInput}. The entity's key string and total data size are available
+ * through this class's {@link #getKey()} and {@link #size()} methods, respectively.
+ * <p class="note">
+ * <strong>Note:</strong> The caller should take care not to seek or close the underlying data
+ * source, nor read more than {@link #size()} bytes from the stream.</p>
+ *
+ * @see BackupAgentHelper
+ * @see BackupHelper
+ */
+public class BackupDataInputStream extends InputStream {
+
+ String key;
+ int dataSize;
+
+ BackupDataInput mData;
+ byte[] mOneByte;
+
+ /** @hide */
+ BackupDataInputStream(BackupDataInput data) {
+ mData = data;
+ }
+
+ /**
+ * Read one byte of entity data from the stream, returning it as
+ * an integer value. If more than {@link #size()} bytes of data
+ * are read from the stream, the output of this method is undefined.
+ *
+ * @return The byte read, or undefined if the end of the stream has been reached.
+ */
+ public int read() throws IOException {
+ byte[] one = mOneByte;
+ if (mOneByte == null) {
+ one = mOneByte = new byte[1];
+ }
+ mData.readEntityData(one, 0, 1);
+ return one[0];
+ }
+
+ /**
+ * Read up to {@code size} bytes of data into a byte array, beginning at position
+ * {@code offset} within the array.
+ *
+ * @param b Byte array into which the data will be read
+ * @param offset The data will be stored in {@code b} beginning at this index
+ * within the array.
+ * @param size The number of bytes to read in this operation. If insufficient
+ * data exists within the entity to fulfill this request, only as much data
+ * will be read as is available.
+ * @return The number of bytes of data read, or zero if all of the entity's
+ * data has already been read.
+ */
+ public int read(byte[] b, int offset, int size) throws IOException {
+ return mData.readEntityData(b, offset, size);
+ }
+
+ /**
+ * Read enough entity data into a byte array to fill the array.
+ *
+ * @param b Byte array to fill with data from the stream. If the stream does not
+ * have sufficient data to fill the array, then the contents of the remainder of
+ * the array will be undefined.
+ * @return The number of bytes of data read, or zero if all of the entity's
+ * data has already been read.
+ */
+ public int read(byte[] b) throws IOException {
+ return mData.readEntityData(b, 0, b.length);
+ }
+
+ /**
+ * Report the key string associated with this entity within the backup data set.
+ *
+ * @return The key string for this entity, equivalent to calling
+ * {@link BackupDataInput#getKey()} on the underlying {@link BackupDataInput}.
+ */
+ public String getKey() {
+ return this.key;
+ }
+
+ /**
+ * Report the total number of bytes of data available for the current entity.
+ *
+ * @return The number of data bytes available, equivalent to calling
+ * {@link BackupDataInput#getDataSize()} on the underlying {@link BackupDataInput}.
+ */
+ public int size() {
+ return this.dataSize;
+ }
+}
+
+
diff --git a/android/app/backup/BackupDataOutput.java b/android/app/backup/BackupDataOutput.java
new file mode 100644
index 00000000..c7586a29
--- /dev/null
+++ b/android/app/backup/BackupDataOutput.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.os.ParcelFileDescriptor;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides the structured interface through which a {@link BackupAgent} commits
+ * information to the backup data set, via its {@link
+ * BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor)
+ * onBackup()} method. Data written for backup is presented
+ * as a set of "entities," key/value pairs in which each binary data record "value" is
+ * named with a string "key."
+ * <p>
+ * To commit a data record to the backup transport, the agent's
+ * {@link BackupAgent#onBackup(ParcelFileDescriptor,BackupDataOutput,ParcelFileDescriptor)
+ * onBackup()} method first writes an "entity header" that supplies the key string for the record
+ * and the total size of the binary value for the record. After the header has been
+ * written, the agent then writes the binary entity value itself. The entity value can
+ * be written in multiple chunks if desired, as long as the total count of bytes written
+ * matches what was supplied to {@link #writeEntityHeader(String, int) writeEntityHeader()}.
+ * <p>
+ * Entity key strings are considered to be unique within a given application's backup
+ * data set. If a backup agent writes a new entity under an existing key string, its value will
+ * replace any previous value in the transport's remote data store. You can remove a record
+ * entirely from the remote data set by writing a new entity header using the
+ * existing record's key, but supplying a negative <code>dataSize</code> parameter.
+ * When you do so, the agent does not need to call {@link #writeEntityData(byte[], int)}.
+ * <h3>Example</h3>
+ * <p>
+ * Here is an example illustrating a way to back up the value of a String variable
+ * called <code>mStringToBackUp</code>:
+ * <pre>
+ * static final String MY_STRING_KEY = "storedstring";
+ *
+ * public void {@link BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)}
+ * throws IOException {
+ * ...
+ * byte[] stringBytes = mStringToBackUp.getBytes();
+ * data.writeEntityHeader(MY_STRING_KEY, stringBytes.length);
+ * data.writeEntityData(stringBytes, stringBytes.length);
+ * ...
+ * }</pre>
+ *
+ * @see BackupAgent
+ */
+public class BackupDataOutput {
+ final long mQuota;
+ long mBackupWriter;
+
+ /**
+ * Construct a BackupDataOutput purely for data-stream manipulation. This instance will
+ * not report usable quota information.
+ * @hide */
+ @SystemApi
+ public BackupDataOutput(FileDescriptor fd) {
+ this(fd, -1);
+ }
+
+ /** @hide */
+ @SystemApi
+ public BackupDataOutput(FileDescriptor fd, long quota) {
+ if (fd == null) throw new NullPointerException();
+ mQuota = quota;
+ mBackupWriter = ctor(fd);
+ if (mBackupWriter == 0) {
+ throw new RuntimeException("Native initialization failed with fd=" + fd);
+ }
+ }
+
+ /**
+ * Returns the quota in bytes for the application's current backup operation. The
+ * value can vary for each operation.
+ *
+ * @see FullBackupDataOutput#getQuota()
+ */
+ public long getQuota() {
+ return mQuota;
+ }
+
+ /**
+ * Mark the beginning of one record in the backup data stream. This must be called before
+ * {@link #writeEntityData}.
+ * @param key A string key that uniquely identifies the data record within the application.
+ * Keys whose first character is \uFF00 or higher are not valid.
+ * @param dataSize The size in bytes of this record's data. Passing a dataSize
+ * of -1 indicates that the record under this key should be deleted.
+ * @return The number of bytes written to the backup stream
+ * @throws IOException if the write failed
+ */
+ public int writeEntityHeader(String key, int dataSize) throws IOException {
+ int result = writeEntityHeader_native(mBackupWriter, key, dataSize);
+ if (result >= 0) {
+ return result;
+ } else {
+ throw new IOException("result=0x" + Integer.toHexString(result));
+ }
+ }
+
+ /**
+ * Write a chunk of data under the current entity to the backup transport.
+ * @param data A raw data buffer to send
+ * @param size The number of bytes to be sent in this chunk
+ * @return the number of bytes written
+ * @throws IOException if the write failed
+ */
+ public int writeEntityData(byte[] data, int size) throws IOException {
+ int result = writeEntityData_native(mBackupWriter, data, size);
+ if (result >= 0) {
+ return result;
+ } else {
+ throw new IOException("result=0x" + Integer.toHexString(result));
+ }
+ }
+
+ /** @hide */
+ public void setKeyPrefix(String keyPrefix) {
+ setKeyPrefix_native(mBackupWriter, keyPrefix);
+ }
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dtor(mBackupWriter);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native static long ctor(FileDescriptor fd);
+ private native static void dtor(long mBackupWriter);
+
+ private native static int writeEntityHeader_native(long mBackupWriter, String key, int dataSize);
+ private native static int writeEntityData_native(long mBackupWriter, byte[] data, int size);
+ private native static void setKeyPrefix_native(long mBackupWriter, String keyPrefix);
+}
+
diff --git a/android/app/backup/BackupHelper.java b/android/app/backup/BackupHelper.java
new file mode 100644
index 00000000..3074a8c8
--- /dev/null
+++ b/android/app/backup/BackupHelper.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Defines the calling interface that {@link BackupAgentHelper} uses
+ * when dispatching backup and restore operations to the installed helpers.
+ * Applications can define and install their own helpers as well as using those
+ * provided as part of the Android framework.
+ * <p>
+ * Although multiple helper objects may be installed simultaneously, each helper
+ * is responsible only for handling its own data, and will not see entities
+ * created by other components within the backup system. Invocations of multiple
+ * helpers are performed sequentially by the {@link BackupAgentHelper}, with each
+ * helper given a chance to access its own saved state from within the state record
+ * produced during the previous backup operation.
+ *
+ * @see BackupAgentHelper
+ * @see FileBackupHelper
+ * @see SharedPreferencesBackupHelper
+ */
+public interface BackupHelper {
+ /**
+ * Based on <code>oldState</code>, determine what application content
+ * needs to be backed up, write it to <code>data</code>, and fill in
+ * <code>newState</code> with the complete state as it exists now.
+ * <p>
+ * Implementing this method is much like implementing
+ * {@link BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)
+ * onBackup()} &mdash; the method parameters are the same. When this method is invoked the
+ * {@code oldState} descriptor points to the beginning of the state data
+ * written during this helper's previous backup operation, and the {@code newState}
+ * descriptor points to the file location at which the helper should write its
+ * new state after performing the backup operation.
+ * <p class="note">
+ * <strong>Note:</strong> The helper should not close or seek either the {@code oldState} or
+ * the {@code newState} file descriptors. It is essential that when reading the helper's
+ * saved state from the {@code oldState} file, no extra content is consumed beyond
+ * what was stored by this helper. If more old state data is read, even accidentally,
+ * it will make it impossible for additional helpers that may be invoked after this one
+ * to properly reconstruct their prior state.</p>
+ *
+ * @param oldState An open, read-only {@link android.os.ParcelFileDescriptor} pointing to the
+ * last backup state provided by the application. May be
+ * <code>null</code>, in which case no prior state is being
+ * provided and the application should perform a full backup.
+ * @param data An open, read/write {@link BackupDataOutput}
+ * pointing to the backup data destination.
+ * Typically the application will use backup helper classes to
+ * write to this file.
+ * @param newState An open, read/write {@link android.os.ParcelFileDescriptor} pointing to an
+ * empty file. The application should record the final backup
+ * state here after writing the requested data to the <code>data</code>
+ * output stream.
+ */
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState);
+
+ /**
+ * Called by {@link android.app.backup.BackupAgentHelper BackupAgentHelper}
+ * to restore a single entity from the restore data set. This method will be
+ * called for each entity in the data set that belongs to this handler.
+ * <p class="note">
+ * <strong>Note:</strong> Do not close the <code>data</code> stream. Do not read more than
+ * {@link android.app.backup.BackupDataInputStream#size() size()} bytes from
+ * <code>data</code>.</p>
+ *
+ * @param data An open {@link BackupDataInputStream} from which the backup data can be read.
+ */
+ public void restoreEntity(BackupDataInputStream data);
+
+ /**
+ * Called by {@link android.app.backup.BackupAgentHelper BackupAgentHelper}
+ * after a restore operation to write the backup state file corresponding to
+ * the data as processed by the helper. The data written here will be
+ * available to the helper during the next call to its
+ * {@link #performBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)
+ * performBackup()} method.
+ * <p>
+ * This method will be called even if the handler's
+ * {@link #restoreEntity(BackupDataInputStream) restoreEntity()} method was never invoked during
+ * the restore operation.
+ * <p class="note">
+ * <strong>Note:</strong> The helper should not close or seek the {@code newState}
+ * file descriptor.</p>
+ *
+ * @param newState A {@link android.os.ParcelFileDescriptor} to which the new state will be
+ * written.
+ */
+ public void writeNewStateDescription(ParcelFileDescriptor newState);
+}
+
diff --git a/android/app/backup/BackupHelperDispatcher.java b/android/app/backup/BackupHelperDispatcher.java
new file mode 100644
index 00000000..68115323
--- /dev/null
+++ b/android/app/backup/BackupHelperDispatcher.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** @hide */
+public class BackupHelperDispatcher {
+ private static final String TAG = "BackupHelperDispatcher";
+
+ private static class Header {
+ int chunkSize; // not including the header
+ String keyPrefix;
+ }
+
+ TreeMap<String,BackupHelper> mHelpers = new TreeMap<String,BackupHelper>();
+
+ public BackupHelperDispatcher() {
+ }
+
+ public void addHelper(String keyPrefix, BackupHelper helper) {
+ mHelpers.put(keyPrefix, helper);
+ }
+
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ // First, do the helpers that we've already done, since they're already in the state
+ // file.
+ int err;
+ Header header = new Header();
+ TreeMap<String,BackupHelper> helpers = (TreeMap<String,BackupHelper>)mHelpers.clone();
+ FileDescriptor oldStateFD = null;
+
+ if (oldState != null) {
+ oldStateFD = oldState.getFileDescriptor();
+ while ((err = readHeader_native(header, oldStateFD)) >= 0) {
+ if (err == 0) {
+ BackupHelper helper = helpers.get(header.keyPrefix);
+ Log.d(TAG, "handling existing helper '" + header.keyPrefix + "' " + helper);
+ if (helper != null) {
+ doOneBackup(oldState, data, newState, header, helper);
+ helpers.remove(header.keyPrefix);
+ } else {
+ skipChunk_native(oldStateFD, header.chunkSize);
+ }
+ }
+ }
+ }
+
+ // Then go through and do the rest that we haven't done.
+ for (Map.Entry<String,BackupHelper> entry: helpers.entrySet()) {
+ header.keyPrefix = entry.getKey();
+ Log.d(TAG, "handling new helper '" + header.keyPrefix + "'");
+ BackupHelper helper = entry.getValue();
+ doOneBackup(oldState, data, newState, header, helper);
+ }
+ }
+
+ private void doOneBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState, Header header, BackupHelper helper)
+ throws IOException {
+ int err;
+ FileDescriptor newStateFD = newState.getFileDescriptor();
+
+ // allocate space for the header in the file
+ int pos = allocateHeader_native(header, newStateFD);
+ if (pos < 0) {
+ throw new IOException("allocateHeader_native failed (error " + pos + ")");
+ }
+
+ data.setKeyPrefix(header.keyPrefix);
+
+ // do the backup
+ helper.performBackup(oldState, data, newState);
+
+ // fill in the header (seeking back to pos). The file pointer will be returned to
+ // where it was at the end of performBackup. Header.chunkSize will not be filled in.
+ err = writeHeader_native(header, newStateFD, pos);
+ if (err != 0) {
+ throw new IOException("writeHeader_native failed (error " + err + ")");
+ }
+ }
+
+ public void performRestore(BackupDataInput input, int appVersionCode,
+ ParcelFileDescriptor newState)
+ throws IOException {
+ boolean alreadyComplained = false;
+
+ BackupDataInputStream stream = new BackupDataInputStream(input);
+ while (input.readNextHeader()) {
+
+ String rawKey = input.getKey();
+ int pos = rawKey.indexOf(':');
+ if (pos > 0) {
+ String prefix = rawKey.substring(0, pos);
+ BackupHelper helper = mHelpers.get(prefix);
+ if (helper != null) {
+ stream.dataSize = input.getDataSize();
+ stream.key = rawKey.substring(pos+1);
+ helper.restoreEntity(stream);
+ } else {
+ if (!alreadyComplained) {
+ Log.w(TAG, "Couldn't find helper for: '" + rawKey + "'");
+ alreadyComplained = true;
+ }
+ }
+ } else {
+ if (!alreadyComplained) {
+ Log.w(TAG, "Entity with no prefix: '" + rawKey + "'");
+ alreadyComplained = true;
+ }
+ }
+ input.skipEntityData(); // In case they didn't consume the data.
+ }
+
+ // Write out the state files -- mHelpers is a TreeMap, so the order is well defined.
+ for (BackupHelper helper: mHelpers.values()) {
+ helper.writeNewStateDescription(newState);
+ }
+ }
+
+ private static native int readHeader_native(Header h, FileDescriptor fd);
+ private static native int skipChunk_native(FileDescriptor fd, int bytesToSkip);
+
+ private static native int allocateHeader_native(Header h, FileDescriptor fd);
+ private static native int writeHeader_native(Header h, FileDescriptor fd, int pos);
+}
+
diff --git a/android/app/backup/BackupManager.java b/android/app/backup/BackupManager.java
new file mode 100644
index 00000000..9f9b2170
--- /dev/null
+++ b/android/app/backup/BackupManager.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Pair;
+
+/**
+ * The interface through which an application interacts with the Android backup service to
+ * request backup and restore operations.
+ * Applications instantiate it using the constructor and issue calls through that instance.
+ * <p>
+ * When an application has made changes to data which should be backed up, a
+ * call to {@link #dataChanged()} will notify the backup service. The system
+ * will then schedule a backup operation to occur in the near future. Repeated
+ * calls to {@link #dataChanged()} have no further effect until the backup
+ * operation actually occurs.
+ * <p>
+ * A backup or restore operation for your application begins when the system launches the
+ * {@link android.app.backup.BackupAgent} subclass you've declared in your manifest. See the
+ * documentation for {@link android.app.backup.BackupAgent} for a detailed description
+ * of how the operation then proceeds.
+ * <p>
+ * Several attributes affecting the operation of the backup and restore mechanism
+ * can be set on the <code>
+ * <a href="{@docRoot}guide/topics/manifest/application-element.html">&lt;application&gt;</a></code>
+ * tag in your application's AndroidManifest.xml file.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using BackupManager, read the
+ * <a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a> developer guide.</p></div>
+ *
+ * @attr ref android.R.styleable#AndroidManifestApplication_allowBackup
+ * @attr ref android.R.styleable#AndroidManifestApplication_backupAgent
+ * @attr ref android.R.styleable#AndroidManifestApplication_killAfterRestore
+ * @attr ref android.R.styleable#AndroidManifestApplication_restoreAnyVersion
+ */
+public class BackupManager {
+ private static final String TAG = "BackupManager";
+
+ // BackupObserver status codes
+ /**
+ * Indicates that backup succeeded.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SUCCESS = 0;
+
+ /**
+ * Indicates that backup is either not enabled at all or
+ * backup for the package was rejected by backup service
+ * or backup transport,
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_BACKUP_NOT_ALLOWED = -2001;
+
+ /**
+ * The requested app is not installed on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_PACKAGE_NOT_FOUND = -2002;
+
+ /**
+ * The backup operation was cancelled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_BACKUP_CANCELLED = -2003;
+
+ /**
+ * The transport for some reason was not in a good state and
+ * aborted the entire backup request. This is a transient
+ * failure and should not be retried immediately.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_TRANSPORT_ABORTED = BackupTransport.TRANSPORT_ERROR;
+
+ /**
+ * Returned when the transport was unable to process the
+ * backup request for a given package, for example if the
+ * transport hit a transient network failure. The remaining
+ * packages provided to {@link #requestBackup(String[], BackupObserver)}
+ * will still be attempted.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_TRANSPORT_PACKAGE_REJECTED =
+ BackupTransport.TRANSPORT_PACKAGE_REJECTED;
+
+ /**
+ * Returned when the transport reject the attempt to backup because
+ * backup data size exceeded current quota limit for this package.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_TRANSPORT_QUOTA_EXCEEDED =
+ BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
+
+ /**
+ * The {@link BackupAgent} for the requested package failed for some reason
+ * and didn't provide appropriate backup data.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_AGENT_FAILURE = BackupTransport.AGENT_ERROR;
+
+ /**
+ * Intent extra when any subsidiary backup-related UI is launched from Settings: does
+ * device policy or configuration permit backup operations to run at all?
+ *
+ * @hide
+ */
+ public static final String EXTRA_BACKUP_SERVICES_AVAILABLE = "backup_services_available";
+
+ /**
+ * If this flag is passed to {@link #requestBackup(String[], BackupObserver, int)},
+ * BackupManager will pass a blank old state to BackupAgents of requested packages.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_NON_INCREMENTAL_BACKUP = 1;
+
+ /**
+ * Use with {@link #requestBackup} to force backup of
+ * package meta data. Typically you do not need to explicitly request this be backed up as it is
+ * handled internally by the BackupManager. If you are requesting backups with
+ * FLAG_NON_INCREMENTAL, this package won't automatically be backed up and you have to
+ * explicitly request for its backup.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String PACKAGE_MANAGER_SENTINEL = "@pm@";
+
+
+ /**
+ * This error code is passed to {@link SelectBackupTransportCallback#onFailure(int)}
+ * if the requested transport is unavailable.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_TRANSPORT_UNAVAILABLE = -1;
+
+ /**
+ * This error code is passed to {@link SelectBackupTransportCallback#onFailure(int)} if the
+ * requested transport is not a valid BackupTransport.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_TRANSPORT_INVALID = -2;
+
+ private Context mContext;
+ private static IBackupManager sService;
+
+ private static void checkServiceBinder() {
+ if (sService == null) {
+ sService = IBackupManager.Stub.asInterface(
+ ServiceManager.getService(Context.BACKUP_SERVICE));
+ }
+ }
+
+ /**
+ * Constructs a BackupManager object through which the application can
+ * communicate with the Android backup system.
+ *
+ * @param context The {@link android.content.Context} that was provided when
+ * one of your application's {@link android.app.Activity Activities}
+ * was created.
+ */
+ public BackupManager(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Notifies the Android backup system that your application wishes to back up
+ * new changes to its data. A backup operation using your application's
+ * {@link android.app.backup.BackupAgent} subclass will be scheduled when you
+ * call this method.
+ */
+ public void dataChanged() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.dataChanged(mContext.getPackageName());
+ } catch (RemoteException e) {
+ Log.d(TAG, "dataChanged() couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Convenience method for callers who need to indicate that some other package
+ * needs a backup pass. This can be useful in the case of groups of packages
+ * that share a uid.
+ * <p>
+ * This method requires that the application hold the "android.permission.BACKUP"
+ * permission if the package named in the argument does not run under the same uid
+ * as the caller.
+ *
+ * @param packageName The package name identifying the application to back up.
+ */
+ public static void dataChanged(String packageName) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.dataChanged(packageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "dataChanged(pkg) couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Restore the calling application from backup. The data will be restored from the
+ * current backup dataset if the application has stored data there, or from
+ * the dataset used during the last full device setup operation if the current
+ * backup dataset has no matching data. If no backup data exists for this application
+ * in either source, a nonzero value will be returned.
+ *
+ * <p>If this method returns zero (meaning success), the OS will attempt to retrieve
+ * a backed-up dataset from the remote transport, instantiate the application's
+ * backup agent, and pass the dataset to the agent's
+ * {@link android.app.backup.BackupAgent#onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor) onRestore()}
+ * method.
+ *
+ * @param observer The {@link RestoreObserver} to receive callbacks during the restore
+ * operation. This must not be null.
+ *
+ * @return Zero on success; nonzero on error.
+ */
+ public int requestRestore(RestoreObserver observer) {
+ return requestRestore(observer, null);
+ }
+
+ // system APIs start here
+
+ /**
+ * Restore the calling application from backup. The data will be restored from the
+ * current backup dataset if the application has stored data there, or from
+ * the dataset used during the last full device setup operation if the current
+ * backup dataset has no matching data. If no backup data exists for this application
+ * in either source, a nonzero value will be returned.
+ *
+ * <p>If this method returns zero (meaning success), the OS will attempt to retrieve
+ * a backed-up dataset from the remote transport, instantiate the application's
+ * backup agent, and pass the dataset to the agent's
+ * {@link android.app.backup.BackupAgent#onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor) onRestore()}
+ * method.
+ *
+ * @param observer The {@link RestoreObserver} to receive callbacks during the restore
+ * operation. This must not be null.
+ *
+ * @param monitor the {@link BackupManagerMonitor} to receive callbacks during the restore
+ * operation.
+ *
+ * @return Zero on success; nonzero on error.
+ *
+ * @hide
+ */
+ @SystemApi
+ public int requestRestore(RestoreObserver observer, BackupManagerMonitor monitor) {
+ int result = -1;
+ checkServiceBinder();
+ if (sService != null) {
+ RestoreSession session = null;
+ try {
+ IRestoreSession binder = sService.beginRestoreSession(mContext.getPackageName(),
+ null);
+ if (binder != null) {
+ session = new RestoreSession(mContext, binder);
+ result = session.restorePackage(mContext.getPackageName(), observer, monitor);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "restoreSelf() unable to contact service");
+ } finally {
+ if (session != null) {
+ session.endRestoreSession();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Begin the process of restoring data from backup. See the
+ * {@link android.app.backup.RestoreSession} class for documentation on that process.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public RestoreSession beginRestoreSession() {
+ RestoreSession session = null;
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ // All packages, current transport
+ IRestoreSession binder = sService.beginRestoreSession(null, null);
+ if (binder != null) {
+ session = new RestoreSession(mContext, binder);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "beginRestoreSession() couldn't connect");
+ }
+ }
+ return session;
+ }
+
+ /**
+ * Enable/disable the backup service entirely. When disabled, no backup
+ * or restore operations will take place. Data-changed notifications will
+ * still be observed and collected, however, so that changes made while the
+ * mechanism was disabled will still be backed up properly if it is enabled
+ * at some point in the future.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void setBackupEnabled(boolean isEnabled) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.setBackupEnabled(isEnabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "setBackupEnabled() couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Report whether the backup mechanism is currently enabled.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public boolean isBackupEnabled() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.isBackupEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, "isBackupEnabled() couldn't connect");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Enable/disable data restore at application install time. When enabled, app
+ * installation will include an attempt to fetch the app's historical data from
+ * the archival restore dataset (if any). When disabled, no such attempt will
+ * be made.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void setAutoRestore(boolean isEnabled) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.setAutoRestore(isEnabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "setAutoRestore() couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Identify the currently selected transport.
+ * @return The name of the currently active backup transport. In case of
+ * failure or if no transport is currently active, this method returns {@code null}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public String getCurrentTransport() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.getCurrentTransport();
+ } catch (RemoteException e) {
+ Log.e(TAG, "getCurrentTransport() couldn't connect");
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Request a list of all available backup transports' names.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public String[] listAllTransports() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.listAllTransports();
+ } catch (RemoteException e) {
+ Log.e(TAG, "listAllTransports() couldn't connect");
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Specify the current backup transport.
+ *
+ * @param transport The name of the transport to select. This should be one
+ * of the names returned by {@link #listAllTransports()}. This is the String returned by
+ * {@link BackupTransport#name()} for the particular transport.
+ * @return The name of the previously selected transport. If the given transport
+ * name is not one of the currently available transports, no change is made to
+ * the current transport setting and the method returns null.
+ *
+ * @hide
+ */
+ @Deprecated
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public String selectBackupTransport(String transport) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.selectBackupTransport(transport);
+ } catch (RemoteException e) {
+ Log.e(TAG, "selectBackupTransport() couldn't connect");
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Specify the current backup transport and get notified when the transport is ready to be used.
+ * This method is async because BackupManager might need to bind to the specified transport
+ * which is in a separate process.
+ *
+ * @param transport ComponentName of the service hosting the transport. This is different from
+ * the transport's name that is returned by {@link BackupTransport#name()}.
+ * @param listener A listener object to get a callback on the transport being selected.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void selectBackupTransport(ComponentName transport,
+ SelectBackupTransportCallback listener) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ SelectTransportListenerWrapper wrapper = listener == null ?
+ null : new SelectTransportListenerWrapper(mContext, listener);
+ sService.selectBackupTransportAsync(transport, wrapper);
+ } catch (RemoteException e) {
+ Log.e(TAG, "selectBackupTransportAsync() couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Schedule an immediate backup attempt for all pending key/value updates. This
+ * is primarily intended for transports to use when they detect a suitable
+ * opportunity for doing a backup pass. If there are no pending updates to
+ * be sent, no action will be taken. Even if some updates are pending, the
+ * transport will still be asked to confirm via the usual requestBackupTime()
+ * method.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void backupNow() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.backupNow();
+ } catch (RemoteException e) {
+ Log.e(TAG, "backupNow() couldn't connect");
+ }
+ }
+ }
+
+ /**
+ * Ask the framework which dataset, if any, the given package's data would be
+ * restored from if we were to install it right now.
+ *
+ * @param packageName The name of the package whose most-suitable dataset we
+ * wish to look up
+ * @return The dataset token from which a restore should be attempted, or zero if
+ * no suitable data is available.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public long getAvailableRestoreToken(String packageName) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.getAvailableRestoreToken(packageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "getAvailableRestoreToken() couldn't connect");
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Ask the framework whether this app is eligible for backup.
+ *
+ * @param packageName The name of the package.
+ * @return Whether this app is eligible for backup.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public boolean isAppEligibleForBackup(String packageName) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.isAppEligibleForBackup(packageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "isAppEligibleForBackup(pkg) couldn't connect");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Request an immediate backup, providing an observer to which results of the backup operation
+ * will be published. The Android backup system will decide for each package whether it will
+ * be full app data backup or key/value-pair-based backup.
+ *
+ * <p>If this method returns {@link BackupManager#SUCCESS}, the OS will attempt to backup all
+ * provided packages using the remote transport.
+ *
+ * @param packages List of package names to backup.
+ * @param observer The {@link BackupObserver} to receive callbacks during the backup
+ * operation. Could be {@code null}.
+ * @return {@link BackupManager#SUCCESS} on success; nonzero on error.
+ * @exception IllegalArgumentException on null or empty {@code packages} param.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public int requestBackup(String[] packages, BackupObserver observer) {
+ return requestBackup(packages, observer, null, 0);
+ }
+
+ /**
+ * Request an immediate backup, providing an observer to which results of the backup operation
+ * will be published. The Android backup system will decide for each package whether it will
+ * be full app data backup or key/value-pair-based backup.
+ *
+ * <p>If this method returns {@link BackupManager#SUCCESS}, the OS will attempt to backup all
+ * provided packages using the remote transport.
+ *
+ * @param packages List of package names to backup.
+ * @param observer The {@link BackupObserver} to receive callbacks during the backup
+ * operation. Could be {@code null}.
+ * @param monitor The {@link BackupManagerMonitorWrapper} to receive callbacks of important
+ * events during the backup operation. Could be {@code null}.
+ * @param flags {@link #FLAG_NON_INCREMENTAL_BACKUP}.
+ * @return {@link BackupManager#SUCCESS} on success; nonzero on error.
+ * @throws IllegalArgumentException on null or empty {@code packages} param.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public int requestBackup(String[] packages, BackupObserver observer,
+ BackupManagerMonitor monitor, int flags) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ BackupObserverWrapper observerWrapper = observer == null
+ ? null
+ : new BackupObserverWrapper(mContext, observer);
+ BackupManagerMonitorWrapper monitorWrapper = monitor == null
+ ? null
+ : new BackupManagerMonitorWrapper(monitor);
+ return sService.requestBackup(packages, observerWrapper, monitorWrapper, flags);
+ } catch (RemoteException e) {
+ Log.e(TAG, "requestBackup() couldn't connect");
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Cancel all running backups. After this call returns, no currently running backups will
+ * interact with the selected transport.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void cancelBackups() {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.cancelBackups();
+ } catch (RemoteException e) {
+ Log.e(TAG, "cancelBackups() couldn't connect.");
+ }
+ }
+ }
+
+ /*
+ * We wrap incoming binder calls with a private class implementation that
+ * redirects them into main-thread actions. This serializes the backup
+ * progress callbacks nicely within the usual main-thread lifecycle pattern.
+ */
+ @SystemApi
+ private class BackupObserverWrapper extends IBackupObserver.Stub {
+ final Handler mHandler;
+ final BackupObserver mObserver;
+
+ static final int MSG_UPDATE = 1;
+ static final int MSG_RESULT = 2;
+ static final int MSG_FINISHED = 3;
+
+ BackupObserverWrapper(Context context, BackupObserver observer) {
+ mHandler = new Handler(context.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE:
+ Pair<String, BackupProgress> obj =
+ (Pair<String, BackupProgress>) msg.obj;
+ mObserver.onUpdate(obj.first, obj.second);
+ break;
+ case MSG_RESULT:
+ mObserver.onResult((String)msg.obj, msg.arg1);
+ break;
+ case MSG_FINISHED:
+ mObserver.backupFinished(msg.arg1);
+ break;
+ default:
+ Log.w(TAG, "Unknown message: " + msg);
+ break;
+ }
+ }
+ };
+ mObserver = observer;
+ }
+
+ // Binder calls into this object just enqueue on the main-thread handler
+ @Override
+ public void onUpdate(String currentPackage, BackupProgress backupProgress) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE, Pair.create(currentPackage, backupProgress)));
+ }
+
+ @Override
+ public void onResult(String currentPackage, int status) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_RESULT, status, 0, currentPackage));
+ }
+
+ @Override
+ public void backupFinished(int status) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_FINISHED, status, 0));
+ }
+ }
+
+ private class SelectTransportListenerWrapper extends ISelectBackupTransportCallback.Stub {
+
+ private final Handler mHandler;
+ private final SelectBackupTransportCallback mListener;
+
+ SelectTransportListenerWrapper(Context context, SelectBackupTransportCallback listener) {
+ mHandler = new Handler(context.getMainLooper());
+ mListener = listener;
+ }
+
+ @Override
+ public void onSuccess(final String transportName) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onSuccess(transportName);
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(final int reason) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onFailure(reason);
+ }
+ });
+ }
+ }
+
+ private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
+ final BackupManagerMonitor mMonitor;
+
+ BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
+ mMonitor = monitor;
+ }
+
+ @Override
+ public void onEvent(final Bundle event) throws RemoteException {
+ mMonitor.onEvent(event);
+ }
+ }
+
+}
diff --git a/android/app/backup/BackupManagerMonitor.java b/android/app/backup/BackupManagerMonitor.java
new file mode 100644
index 00000000..ebad16e0
--- /dev/null
+++ b/android/app/backup/BackupManagerMonitor.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.os.Bundle;
+
+/**
+ * Callback class for receiving important events during backup/restore operations.
+ * Events consist mostly of errors and exceptions, giving detailed reason on why a restore/backup
+ * failed or any time BackupManager makes an important decision.
+ * On the other hand {@link BackupObserver} will give a failure/success view without
+ * getting into details why. This callback runs on the thread it was called on because it can get
+ * a bit spammy.
+ * These callbacks will run on the binder thread.
+ *
+ * @hide
+ */
+@SystemApi
+public class BackupManagerMonitor {
+
+ // Logging constants for BackupManagerMonitor
+ public static final int LOG_EVENT_CATEGORY_TRANSPORT = 1;
+ public static final int LOG_EVENT_CATEGORY_AGENT = 2;
+ public static final int LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY = 3;
+ /** string : the package name */
+ public static final String EXTRA_LOG_EVENT_PACKAGE_NAME =
+ "android.app.backup.extra.LOG_EVENT_PACKAGE_NAME";
+ /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */
+ public static final String EXTRA_LOG_EVENT_PACKAGE_VERSION =
+ "android.app.backup.extra.LOG_EVENT_PACKAGE_VERSION";
+ /** int : the id of the log message, will be a unique identifier */
+ public static final String EXTRA_LOG_EVENT_ID = "android.app.backup.extra.LOG_EVENT_ID";
+ /**
+ * int : category will be one of
+ * { LOG_EVENT_CATEGORY_TRANSPORT,
+ * LOG_EVENT_CATEGORY_AGENT,
+ * LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY}.
+ */
+ public static final String EXTRA_LOG_EVENT_CATEGORY =
+ "android.app.backup.extra.LOG_EVENT_CATEGORY";
+
+
+ /**
+ * boolean: when we have an event with id LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL we record if
+ * the call was to cancel backup of all packages
+ */
+ public static final String EXTRA_LOG_CANCEL_ALL = "android.app.backup.extra.LOG_CANCEL_ALL";
+
+ /**
+ * string: when we have an event with id LOG_EVENT_ID_ILLEGAL_KEY we send the key that was used
+ * by the app
+ */
+ public static final String EXTRA_LOG_ILLEGAL_KEY = "android.app.backup.extra.LOG_ILLEGAL_KEY";
+
+ /**
+ * long: when we have an event with id LOG_EVENT_ID_ERROR_PREFLIGHT we send the error code that
+ * was returned by the transport during preflight
+ */
+ public static final String EXTRA_LOG_PREFLIGHT_ERROR =
+ "android.app.backup.extra.LOG_PREFLIGHT_ERROR";
+
+ /**
+ * string: when we have an event with id LOG_EVENT_ID_EXCEPTION_FULL_BACKUP we send the
+ * exception's stacktrace
+ */
+ public static final String EXTRA_LOG_EXCEPTION_FULL_BACKUP =
+ "android.app.backup.extra.LOG_EXCEPTION_FULL_BACKUP";
+
+ /**
+ * int: when we have an event with id LOG_EVENT_ID_RESTORE_VERSION_HIGHER we send the
+ * restore package version
+ */
+ public static final String EXTRA_LOG_RESTORE_VERSION =
+ "android.app.backup.extra.LOG_RESTORE_VERSION";
+
+ /**
+ * boolean: when we have an event with id LOG_EVENT_ID_RESTORE_VERSION_HIGHER we record if
+ * ApplicationInfo.FLAG_RESTORE_ANY_VERSION flag is set
+ */
+ public static final String EXTRA_LOG_RESTORE_ANYWAY =
+ "android.app.backup.extra.LOG_RESTORE_ANYWAY";
+
+
+ /**
+ * boolean: when we have an event with id LOG_EVENT_ID_APK_NOT_INSTALLED we record if
+ * the policy allows to install apks provided with the dataset
+ */
+ public static final String EXTRA_LOG_POLICY_ALLOW_APKS =
+ "android.app.backup.extra.LOG_POLICY_ALLOW_APKS";
+
+
+ /**
+ * string: when we have an event with id LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE we record the
+ * package name provided in the restore manifest
+ */
+ public static final String EXTRA_LOG_MANIFEST_PACKAGE_NAME =
+ "android.app.backup.extra.LOG_MANIFEST_PACKAGE_NAME";
+
+ /**
+ * string: when we have an event with id LOG_EVENT_ID_WIDGET_METADATA_MISMATCH we record the
+ * package name provided in the widget metadata
+ */
+ public static final String EXTRA_LOG_WIDGET_PACKAGE_NAME =
+ "android.app.backup.extra.LOG_WIDGET_PACKAGE_NAME";
+
+ /**
+ * int: when we have event of id LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER we send the version
+ * of the backup.
+ */
+ public static final String EXTRA_LOG_OLD_VERSION = "android.app.backup.extra.LOG_OLD_VERSION";
+
+ // TODO complete this list with all log messages. And document properly.
+ public static final int LOG_EVENT_ID_FULL_BACKUP_CANCEL = 4;
+ public static final int LOG_EVENT_ID_ILLEGAL_KEY = 5;
+ public static final int LOG_EVENT_ID_NO_DATA_TO_SEND = 7;
+ public static final int LOG_EVENT_ID_PACKAGE_INELIGIBLE = 9;
+ public static final int LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT = 10;
+ public static final int LOG_EVENT_ID_PACKAGE_STOPPED = 11;
+ public static final int LOG_EVENT_ID_PACKAGE_NOT_FOUND = 12;
+ public static final int LOG_EVENT_ID_BACKUP_DISABLED = 13;
+ public static final int LOG_EVENT_ID_DEVICE_NOT_PROVISIONED = 14;
+ public static final int LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT = 15;
+ public static final int LOG_EVENT_ID_ERROR_PREFLIGHT = 16;
+ public static final int LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT = 18;
+ public static final int LOG_EVENT_ID_EXCEPTION_FULL_BACKUP = 19;
+ public static final int LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL = 21;
+ public static final int LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE = 22;
+ public static final int LOG_EVENT_ID_NO_PM_METADATA_RECEIVED = 23;
+ public static final int LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA = 24;
+ public static final int LOG_EVENT_ID_LOST_TRANSPORT = 25;
+ public static final int LOG_EVENT_ID_PACKAGE_NOT_PRESENT = 26;
+ public static final int LOG_EVENT_ID_RESTORE_VERSION_HIGHER = 27;
+ public static final int LOG_EVENT_ID_APP_HAS_NO_AGENT = 28;
+ public static final int LOG_EVENT_ID_SIGNATURE_MISMATCH = 29;
+ public static final int LOG_EVENT_ID_CANT_FIND_AGENT = 30;
+ public static final int LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT = 31;
+ public static final int LOG_EVENT_ID_RESTORE_ANY_VERSION = 34;
+ public static final int LOG_EVENT_ID_VERSIONS_MATCH = 35;
+ public static final int LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER = 36;
+ public static final int LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH = 37;
+ public static final int LOG_EVENT_ID_SYSTEM_APP_NO_AGENT = 38;
+ public static final int LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE = 39;
+ public static final int LOG_EVENT_ID_APK_NOT_INSTALLED = 40;
+ public static final int LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK = 41;
+ public static final int LOG_EVENT_ID_MISSING_SIGNATURE = 42;
+ public static final int LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE = 43;
+ public static final int LOG_EVENT_ID_UNKNOWN_VERSION = 44;
+ public static final int LOG_EVENT_ID_FULL_RESTORE_TIMEOUT = 45;
+ public static final int LOG_EVENT_ID_CORRUPT_MANIFEST = 46;
+ public static final int LOG_EVENT_ID_WIDGET_METADATA_MISMATCH = 47;
+ public static final int LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION = 48;
+ public static final int LOG_EVENT_ID_NO_PACKAGES = 49;
+ public static final int LOG_EVENT_ID_TRANSPORT_IS_NULL = 50;
+
+
+
+
+
+ /**
+ * This method will be called each time something important happens on BackupManager.
+ *
+ * @param event bundle will contain data about event:
+ * - event id, not optional, a unique identifier for each event.
+ * - package name, optional, the current package we're backing up/restoring if applicable.
+ * - package version, optional, the current package version we're backing up/restoring
+ * if applicable.
+ * - category of event, not optional, one of
+ * { LOG_EVENT_CATEGORY_TRANSPORT,
+ * LOG_EVENT_CATEGORY_AGENT,
+ * LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY}
+ *
+ */
+ public void onEvent(Bundle event) {
+ }
+}
diff --git a/android/app/backup/BackupObserver.java b/android/app/backup/BackupObserver.java
new file mode 100644
index 00000000..0dd071e9
--- /dev/null
+++ b/android/app/backup/BackupObserver.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+
+/**
+ * Callback class for receiving progress reports during a backup operation. These
+ * methods will all be called on your application's main thread.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class BackupObserver {
+ /**
+ * This method could be called several times for packages with full data backup.
+ * It will tell how much of backup data is already saved and how much is expected.
+ *
+ * @param currentBackupPackage The name of the package that now being backuped.
+ * @param backupProgress Current progress of backup for the package.
+ */
+ public void onUpdate(String currentBackupPackage, BackupProgress backupProgress) {
+ }
+
+ /**
+ * The backup of single package has completed. This method will be called at most one time
+ * for each package and could be not called if backup is failed before and
+ * backupFinished() is called.
+ *
+ * @param currentBackupPackage The name of the package that was backuped.
+ * @param status Zero on success; a nonzero error code if the backup operation failed.
+ */
+ public void onResult(String currentBackupPackage, int status) {
+ }
+
+ /**
+ * The backup process has completed. This method will always be called,
+ * even if no individual package backup operations were attempted.
+ *
+ * @param status Zero on success; a nonzero error code if the backup operation
+ * as a whole failed.
+ */
+ public void backupFinished(int status) {
+ }
+}
diff --git a/android/app/backup/BackupProgress.java b/android/app/backup/BackupProgress.java
new file mode 100644
index 00000000..32e62126
--- /dev/null
+++ b/android/app/backup/BackupProgress.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information about current progress of full data backup
+ * Used in {@link BackupObserver#onUpdate(String, BackupProgress)}
+ *
+ * @hide
+ */
+@SystemApi
+public class BackupProgress implements Parcelable {
+
+ /**
+ * Expected size of data in full backup.
+ */
+ public final long bytesExpected;
+ /**
+ * Amount of backup data that is already saved in backup.
+ */
+ public final long bytesTransferred;
+
+ public BackupProgress(long _bytesExpected, long _bytesTransferred) {
+ bytesExpected = _bytesExpected;
+ bytesTransferred = _bytesTransferred;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(bytesExpected);
+ out.writeLong(bytesTransferred);
+ }
+
+ public static final Creator<BackupProgress> CREATOR = new Creator<BackupProgress>() {
+ public BackupProgress createFromParcel(Parcel in) {
+ return new BackupProgress(in);
+ }
+
+ public BackupProgress[] newArray(int size) {
+ return new BackupProgress[size];
+ }
+ };
+
+ private BackupProgress(Parcel in) {
+ bytesExpected = in.readLong();
+ bytesTransferred = in.readLong();
+ }
+}
diff --git a/android/app/backup/BackupTransport.java b/android/app/backup/BackupTransport.java
new file mode 100644
index 00000000..da81d19c
--- /dev/null
+++ b/android/app/backup/BackupTransport.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+
+import com.android.internal.backup.IBackupTransport;
+
+/**
+ * Concrete class that provides a stable-API bridge between IBackupTransport
+ * and its implementations.
+ *
+ * @hide
+ */
+@SystemApi
+public class BackupTransport {
+ // Zero return always means things are okay. If returned from
+ // getNextFullRestoreDataChunk(), it means that no data could be delivered at
+ // this time, but the restore is still running and the caller should simply
+ // retry.
+ public static final int TRANSPORT_OK = 0;
+
+ // -1 is special; it is used in getNextFullRestoreDataChunk() to indicate that
+ // we've delivered the entire data stream for the current restore target.
+ public static final int NO_MORE_DATA = -1;
+
+ // Result codes that indicate real errors are negative and not -1
+ public static final int TRANSPORT_ERROR = -1000;
+ public static final int TRANSPORT_NOT_INITIALIZED = -1001;
+ public static final int TRANSPORT_PACKAGE_REJECTED = -1002;
+ public static final int AGENT_ERROR = -1003;
+ public static final int AGENT_UNKNOWN = -1004;
+ public static final int TRANSPORT_QUOTA_EXCEEDED = -1005;
+
+ // Indicates that operation was initiated by user, not a scheduled one.
+ // Transport should ignore its own moratoriums for call with this flag set.
+ public static final int FLAG_USER_INITIATED = 1;
+
+ IBackupTransport mBinderImpl = new TransportImpl();
+
+ public IBinder getBinder() {
+ return mBinderImpl.asBinder();
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Transport self-description and general configuration interfaces
+ //
+
+ /**
+ * Ask the transport for the name under which it should be registered. This will
+ * typically be its host service's component name, but need not be.
+ */
+ public String name() {
+ throw new UnsupportedOperationException("Transport name() not implemented");
+ }
+
+ /**
+ * Ask the transport for an Intent that can be used to launch any internal
+ * configuration Activity that it wishes to present. For example, the transport
+ * may offer a UI for allowing the user to supply login credentials for the
+ * transport's off-device backend.
+ *
+ * <p>If the transport does not supply any user-facing configuration UI, it should
+ * return {@code null} from this method.
+ *
+ * @return An Intent that can be passed to Context.startActivity() in order to
+ * launch the transport's configuration UI. This method will return {@code null}
+ * if the transport does not offer any user-facing configuration UI.
+ */
+ public Intent configurationIntent() {
+ return null;
+ }
+
+ /**
+ * On demand, supply a one-line string that can be shown to the user that
+ * describes the current backend destination. For example, a transport that
+ * can potentially associate backup data with arbitrary user accounts should
+ * include the name of the currently-active account here.
+ *
+ * @return A string describing the destination to which the transport is currently
+ * sending data. This method should not return null.
+ */
+ public String currentDestinationString() {
+ throw new UnsupportedOperationException(
+ "Transport currentDestinationString() not implemented");
+ }
+
+ /**
+ * Ask the transport for an Intent that can be used to launch a more detailed
+ * secondary data management activity. For example, the configuration intent might
+ * be one for allowing the user to select which account they wish to associate
+ * their backups with, and the management intent might be one which presents a
+ * UI for managing the data on the backend.
+ *
+ * <p>In the Settings UI, the configuration intent will typically be invoked
+ * when the user taps on the preferences item labeled with the current
+ * destination string, and the management intent will be placed in an overflow
+ * menu labelled with the management label string.
+ *
+ * <p>If the transport does not supply any user-facing data management
+ * UI, then it should return {@code null} from this method.
+ *
+ * @return An intent that can be passed to Context.startActivity() in order to
+ * launch the transport's data-management UI. This method will return
+ * {@code null} if the transport does not offer any user-facing data
+ * management UI.
+ */
+ public Intent dataManagementIntent() {
+ return null;
+ }
+
+ /**
+ * On demand, supply a short string that can be shown to the user as the label
+ * on an overflow menu item used to invoked the data management UI.
+ *
+ * @return A string to be used as the label for the transport's data management
+ * affordance. If the transport supplies a data management intent, this
+ * method must not return {@code null}.
+ */
+ public String dataManagementLabel() {
+ throw new UnsupportedOperationException(
+ "Transport dataManagementLabel() not implemented");
+ }
+
+ /**
+ * Ask the transport where, on local device storage, to keep backup state blobs.
+ * This is per-transport so that mock transports used for testing can coexist with
+ * "live" backup services without interfering with the live bookkeeping. The
+ * returned string should be a name that is expected to be unambiguous among all
+ * available backup transports; the name of the class implementing the transport
+ * is a good choice.
+ *
+ * @return A unique name, suitable for use as a file or directory name, that the
+ * Backup Manager could use to disambiguate state files associated with
+ * different backup transports.
+ */
+ public String transportDirName() {
+ throw new UnsupportedOperationException(
+ "Transport transportDirName() not implemented");
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Device-level operations common to both key/value and full-data storage
+
+ /**
+ * Initialize the server side storage for this device, erasing all stored data.
+ * The transport may send the request immediately, or may buffer it. After
+ * this is called, {@link #finishBackup} will be called to ensure the request
+ * is sent and received successfully.
+ *
+ * <p>If the transport returns anything other than TRANSPORT_OK from this method,
+ * the OS will halt the current initialize operation and schedule a retry in the
+ * near future. Even if the transport is in a state such that attempting to
+ * "initialize" the backend storage is meaningless -- for example, if there is
+ * no current live dataset at all, or there is no authenticated account under which
+ * to store the data remotely -- the transport should return TRANSPORT_OK here
+ * and treat the initializeDevice() / finishBackup() pair as a graceful no-op.
+ *
+ * @return One of {@link BackupTransport#TRANSPORT_OK} (OK so far) or
+ * {@link BackupTransport#TRANSPORT_ERROR} (to retry following network error
+ * or other failure).
+ */
+ public int initializeDevice() {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ /**
+ * Erase the given application's data from the backup destination. This clears
+ * out the given package's data from the current backup set, making it as though
+ * the app had never yet been backed up. After this is called, {@link finishBackup}
+ * must be called to ensure that the operation is recorded successfully.
+ *
+ * @return the same error codes as {@link #performBackup}.
+ */
+ public int clearBackupData(PackageInfo packageInfo) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ /**
+ * Finish sending application data to the backup destination. This must be
+ * called after {@link #performBackup}, {@link #performFullBackup}, or {@link clearBackupData}
+ * to ensure that all data is sent and the operation properly finalized. Only when this
+ * method returns true can a backup be assumed to have succeeded.
+ *
+ * @return the same error codes as {@link #performBackup} or {@link #performFullBackup}.
+ */
+ public int finishBackup() {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Key/value incremental backup support interfaces
+
+ /**
+ * Verify that this is a suitable time for a key/value backup pass. This should return zero
+ * if a backup is reasonable right now, some positive value otherwise. This method
+ * will be called outside of the {@link #performBackup}/{@link #finishBackup} pair.
+ *
+ * <p>If this is not a suitable time for a backup, the transport should return a
+ * backoff delay, in milliseconds, after which the Backup Manager should try again.
+ *
+ * @return Zero if this is a suitable time for a backup pass, or a positive time delay
+ * in milliseconds to suggest deferring the backup pass for a while.
+ */
+ public long requestBackupTime() {
+ return 0;
+ }
+
+ /**
+ * Send one application's key/value data update to the backup destination. The
+ * transport may send the data immediately, or may buffer it. If this method returns
+ * {@link #TRANSPORT_OK}, {@link #finishBackup} will then be called to ensure the data
+ * is sent and recorded successfully.
+ *
+ * @param packageInfo The identity of the application whose data is being backed up.
+ * This specifically includes the signature list for the package.
+ * @param inFd Descriptor of file with data that resulted from invoking the application's
+ * BackupService.doBackup() method. This may be a pipe rather than a file on
+ * persistent media, so it may not be seekable.
+ * @param flags {@link BackupTransport#FLAG_USER_INITIATED} or 0.
+ * @return one of {@link BackupTransport#TRANSPORT_OK} (OK so far),
+ * {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} (to suppress backup of this
+ * specific package, but allow others to proceed),
+ * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), or
+ * {@link BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has
+ * become lost due to inactivity purge or some other reason and needs re-initializing)
+ */
+ public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags) {
+ return performBackup(packageInfo, inFd);
+ }
+
+ /**
+ * Legacy version of {@link #performBackup(PackageInfo, ParcelFileDescriptor, int)} that
+ * doesn't use flags parameter.
+ */
+ public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Key/value dataset restore interfaces
+
+ /**
+ * Get the set of all backups currently available over this transport.
+ *
+ * @return Descriptions of the set of restore images available for this device,
+ * or null if an error occurred (the attempt should be rescheduled).
+ **/
+ public RestoreSet[] getAvailableRestoreSets() {
+ return null;
+ }
+
+ /**
+ * Get the identifying token of the backup set currently being stored from
+ * this device. This is used in the case of applications wishing to restore
+ * their last-known-good data.
+ *
+ * @return A token that can be passed to {@link #startRestore}, or 0 if there
+ * is no backup set available corresponding to the current device state.
+ */
+ public long getCurrentRestoreSet() {
+ return 0;
+ }
+
+ /**
+ * Start restoring application data from backup. After calling this function,
+ * alternate calls to {@link #nextRestorePackage} and {@link #nextRestoreData}
+ * to walk through the actual application data.
+ *
+ * @param token A backup token as returned by {@link #getAvailableRestoreSets}
+ * or {@link #getCurrentRestoreSet}.
+ * @param packages List of applications to restore (if data is available).
+ * Application data will be restored in the order given.
+ * @return One of {@link BackupTransport#TRANSPORT_OK} (OK so far, call
+ * {@link #nextRestorePackage}) or {@link BackupTransport#TRANSPORT_ERROR}
+ * (an error occurred, the restore should be aborted and rescheduled).
+ */
+ public int startRestore(long token, PackageInfo[] packages) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ /**
+ * Get the package name of the next application with data in the backup store, plus
+ * a description of the structure of the restored archive: either TYPE_KEY_VALUE for
+ * an original-API key/value dataset, or TYPE_FULL_STREAM for a tarball-type archive stream.
+ *
+ * <p>If the package name in the returned RestoreDescription object is the singleton
+ * {@link RestoreDescription#NO_MORE_PACKAGES}, it indicates that no further data is available
+ * in the current restore session: all packages described in startRestore() have been
+ * processed.
+ *
+ * <p>If this method returns {@code null}, it means that a transport-level error has
+ * occurred and the entire restore operation should be abandoned.
+ *
+ * <p class="note">The OS may call {@link #nextRestorePackage()} multiple times
+ * before calling either {@link #getRestoreData(ParcelFileDescriptor) getRestoreData()}
+ * or {@link #getNextFullRestoreDataChunk(ParcelFileDescriptor) getNextFullRestoreDataChunk()}.
+ * It does this when it has determined that it needs to skip restore of one or more
+ * packages. The transport should not actually transfer any restore data for
+ * the given package in response to {@link #nextRestorePackage()}, but rather wait
+ * for an explicit request before doing so.
+ *
+ * @return A RestoreDescription object containing the name of one of the packages
+ * supplied to {@link #startRestore} plus an indicator of the data type of that
+ * restore data; or {@link RestoreDescription#NO_MORE_PACKAGES} to indicate that
+ * no more packages can be restored in this session; or {@code null} to indicate
+ * a transport-level error.
+ */
+ public RestoreDescription nextRestorePackage() {
+ return null;
+ }
+
+ /**
+ * Get the data for the application returned by {@link #nextRestorePackage}, if that
+ * method reported {@link RestoreDescription#TYPE_KEY_VALUE} as its delivery type.
+ * If the package has only TYPE_FULL_STREAM data, then this method will return an
+ * error.
+ *
+ * @param data An open, writable file into which the key/value backup data should be stored.
+ * @return the same error codes as {@link #startRestore}.
+ */
+ public int getRestoreData(ParcelFileDescriptor outFd) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ /**
+ * End a restore session (aborting any in-process data transfer as necessary),
+ * freeing any resources and connections used during the restore process.
+ */
+ public void finishRestore() {
+ throw new UnsupportedOperationException(
+ "Transport finishRestore() not implemented");
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Full backup interfaces
+
+ /**
+ * Verify that this is a suitable time for a full-data backup pass. This should return zero
+ * if a backup is reasonable right now, some positive value otherwise. This method
+ * will be called outside of the {@link #performFullBackup}/{@link #finishBackup} pair.
+ *
+ * <p>If this is not a suitable time for a backup, the transport should return a
+ * backoff delay, in milliseconds, after which the Backup Manager should try again.
+ *
+ * @return Zero if this is a suitable time for a backup pass, or a positive time delay
+ * in milliseconds to suggest deferring the backup pass for a while.
+ *
+ * @see #requestBackupTime()
+ */
+ public long requestFullBackupTime() {
+ return 0;
+ }
+
+ /**
+ * Begin the process of sending an application's full-data archive to the backend.
+ * The description of the package whose data will be delivered is provided, as well as
+ * the socket file descriptor on which the transport will receive the data itself.
+ *
+ * <p>If the package is not eligible for backup, the transport should return
+ * {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED}. In this case the system will
+ * simply proceed with the next candidate if any, or finish the full backup operation
+ * if all apps have been processed.
+ *
+ * <p>After the transport returns {@link BackupTransport#TRANSPORT_OK} from this
+ * method, the OS will proceed to call {@link #sendBackupData()} one or more times
+ * to deliver the application's data as a streamed tarball. The transport should not
+ * read() from the socket except as instructed to via the {@link #sendBackupData(int)}
+ * method.
+ *
+ * <p>After all data has been delivered to the transport, the system will call
+ * {@link #finishBackup()}. At this point the transport should commit the data to
+ * its datastore, if appropriate, and close the socket that had been provided in
+ * {@link #performFullBackup(PackageInfo, ParcelFileDescriptor)}.
+ *
+ * <p class="note">If the transport returns TRANSPORT_OK from this method, then the
+ * OS will always provide a matching call to {@link #finishBackup()} even if sending
+ * data via {@link #sendBackupData(int)} failed at some point.
+ *
+ * @param targetPackage The package whose data is to follow.
+ * @param socket The socket file descriptor through which the data will be provided.
+ * If the transport returns {@link #TRANSPORT_PACKAGE_REJECTED} here, it must still
+ * close this file descriptor now; otherwise it should be cached for use during
+ * succeeding calls to {@link #sendBackupData(int)}, and closed in response to
+ * {@link #finishBackup()}.
+ * @param flags {@link BackupTransport#FLAG_USER_INITIATED} or 0.
+ * @return TRANSPORT_PACKAGE_REJECTED to indicate that the stated application is not
+ * to be backed up; TRANSPORT_OK to indicate that the OS may proceed with delivering
+ * backup data; TRANSPORT_ERROR to indicate a fatal error condition that precludes
+ * performing a backup at this time.
+ */
+ public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket,
+ int flags) {
+ return performFullBackup(targetPackage, socket);
+ }
+
+ /**
+ * Legacy version of {@link #performFullBackup(PackageInfo, ParcelFileDescriptor, int)} that
+ * doesn't use flags parameter.
+ */
+ public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) {
+ return BackupTransport.TRANSPORT_PACKAGE_REJECTED;
+ }
+
+ /**
+ * Called after {@link #performFullBackup} to make sure that the transport is willing to
+ * handle a full-data backup operation of the specified size on the current package.
+ * If the transport returns anything other than TRANSPORT_OK, the package's backup
+ * operation will be skipped (and {@link #finishBackup() invoked} with no data for that
+ * package being passed to {@link #sendBackupData}.
+ *
+ * <p class="note">The platform does no size-based rejection of full backup attempts on
+ * its own: it is always the responsibility of the transport to implement its own policy.
+ * In particular, even if the preflighted payload size is zero, the platform will still call
+ * this method and will proceed to back up an archive metadata header with no file content
+ * if this method returns TRANSPORT_OK. To avoid storing such payloads the transport
+ * must recognize this case and return TRANSPORT_PACKAGE_REJECTED.
+ *
+ * Added in {@link android.os.Build.VERSION_CODES#M}.
+ *
+ * @param size The estimated size of the full-data payload for this app. This includes
+ * manifest and archive format overhead, but is not guaranteed to be precise.
+ * @return TRANSPORT_OK if the platform is to proceed with the full-data backup,
+ * TRANSPORT_PACKAGE_REJECTED if the proposed payload size is too large for
+ * the transport to handle, or TRANSPORT_ERROR to indicate a fatal error
+ * condition that means the platform cannot perform a backup at this time.
+ */
+ public int checkFullBackupSize(long size) {
+ return BackupTransport.TRANSPORT_OK;
+ }
+
+ /**
+ * Tells the transport to read {@code numBytes} bytes of data from the socket file
+ * descriptor provided in the {@link #performFullBackup(PackageInfo, ParcelFileDescriptor)}
+ * call, and deliver those bytes to the datastore.
+ *
+ * @param numBytes The number of bytes of tarball data available to be read from the
+ * socket.
+ * @return TRANSPORT_OK on successful processing of the data; TRANSPORT_ERROR to
+ * indicate a fatal error situation. If an error is returned, the system will
+ * call finishBackup() and stop attempting backups until after a backoff and retry
+ * interval.
+ */
+ public int sendBackupData(int numBytes) {
+ return BackupTransport.TRANSPORT_ERROR;
+ }
+
+ /**
+ * Tells the transport to cancel the currently-ongoing full backup operation. This
+ * will happen between {@link #performFullBackup()} and {@link #finishBackup()}
+ * if the OS needs to abort the backup operation for any reason, such as a crash in
+ * the application undergoing backup.
+ *
+ * <p>When it receives this call, the transport should discard any partial archive
+ * that it has stored so far. If possible it should also roll back to the previous
+ * known-good archive in its datastore.
+ *
+ * <p>If the transport receives this callback, it will <em>not</em> receive a
+ * call to {@link #finishBackup()}. It needs to tear down any ongoing backup state
+ * here.
+ */
+ public void cancelFullBackup() {
+ throw new UnsupportedOperationException(
+ "Transport cancelFullBackup() not implemented");
+ }
+
+ /**
+ * Ask the transport whether this app is eligible for backup.
+ *
+ * @param targetPackage The identity of the application.
+ * @param isFullBackup If set, transport should check if app is eligible for full data backup,
+ * otherwise to check if eligible for key-value backup.
+ * @return Whether this app is eligible for backup.
+ */
+ public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup) {
+ return true;
+ }
+
+ /**
+ * Ask the transport about current quota for backup size of the package.
+ *
+ * @param packageName ID of package to provide the quota.
+ * @param isFullBackup If set, transport should return limit for full data backup, otherwise
+ * for key-value backup.
+ * @return Current limit on backup size in bytes.
+ */
+ public long getBackupQuota(String packageName, boolean isFullBackup) {
+ return Long.MAX_VALUE;
+ }
+
+ // ------------------------------------------------------------------------------------
+ // Full restore interfaces
+
+ /**
+ * Ask the transport to provide data for the "current" package being restored. This
+ * is the package that was just reported by {@link #nextRestorePackage()} as having
+ * {@link RestoreDescription#TYPE_FULL_STREAM} data.
+ *
+ * The transport writes some data to the socket supplied to this call, and returns
+ * the number of bytes written. The system will then read that many bytes and
+ * stream them to the application's agent for restore, then will call this method again
+ * to receive the next chunk of the archive. This sequence will be repeated until the
+ * transport returns zero indicating that all of the package's data has been delivered
+ * (or returns a negative value indicating some sort of hard error condition at the
+ * transport level).
+ *
+ * <p>After this method returns zero, the system will then call
+ * {@link #nextRestorePackage()} to begin the restore process for the next
+ * application, and the sequence begins again.
+ *
+ * <p>The transport should always close this socket when returning from this method.
+ * Do not cache this socket across multiple calls or you may leak file descriptors.
+ *
+ * @param socket The file descriptor that the transport will use for delivering the
+ * streamed archive. The transport must close this socket in all cases when returning
+ * from this method.
+ * @return {@link #NO_MORE_DATA} when no more data for the current package is available.
+ * A positive value indicates the presence of that many bytes to be delivered to the app.
+ * A value of zero indicates that no data was deliverable at this time, but the restore
+ * is still running and the caller should retry. {@link #TRANSPORT_PACKAGE_REJECTED}
+ * means that the current package's restore operation should be aborted, but that
+ * the transport itself is still in a good state and so a multiple-package restore
+ * sequence can still be continued. Any other negative return value is treated as a
+ * fatal error condition that aborts all further restore operations on the current dataset.
+ */
+ public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
+ return 0;
+ }
+
+ /**
+ * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM}
+ * data for restore, it will invoke this method to tell the transport that it should
+ * abandon the data download for the current package. The OS will then either call
+ * {@link #nextRestorePackage()} again to move on to restoring the next package in the
+ * set being iterated over, or will call {@link #finishRestore()} to shut down the restore
+ * operation.
+ *
+ * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the
+ * current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious
+ * transport-level failure. If the transport reports an error here, the entire restore
+ * operation will immediately be finished with no further attempts to restore app data.
+ */
+ public int abortFullRestore() {
+ return BackupTransport.TRANSPORT_OK;
+ }
+
+ /**
+ * Bridge between the actual IBackupTransport implementation and the stable API. If the
+ * binder interface needs to change, we use this layer to translate so that we can
+ * (if appropriate) decouple those framework-side changes from the BackupTransport
+ * implementations.
+ */
+ class TransportImpl extends IBackupTransport.Stub {
+
+ @Override
+ public String name() throws RemoteException {
+ return BackupTransport.this.name();
+ }
+
+ @Override
+ public Intent configurationIntent() throws RemoteException {
+ return BackupTransport.this.configurationIntent();
+ }
+
+ @Override
+ public String currentDestinationString() throws RemoteException {
+ return BackupTransport.this.currentDestinationString();
+ }
+
+ @Override
+ public Intent dataManagementIntent() {
+ return BackupTransport.this.dataManagementIntent();
+ }
+
+ @Override
+ public String dataManagementLabel() {
+ return BackupTransport.this.dataManagementLabel();
+ }
+
+ @Override
+ public String transportDirName() throws RemoteException {
+ return BackupTransport.this.transportDirName();
+ }
+
+ @Override
+ public long requestBackupTime() throws RemoteException {
+ return BackupTransport.this.requestBackupTime();
+ }
+
+ @Override
+ public int initializeDevice() throws RemoteException {
+ return BackupTransport.this.initializeDevice();
+ }
+
+ @Override
+ public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags)
+ throws RemoteException {
+ return BackupTransport.this.performBackup(packageInfo, inFd, flags);
+ }
+
+ @Override
+ public int clearBackupData(PackageInfo packageInfo) throws RemoteException {
+ return BackupTransport.this.clearBackupData(packageInfo);
+ }
+
+ @Override
+ public int finishBackup() throws RemoteException {
+ return BackupTransport.this.finishBackup();
+ }
+
+ @Override
+ public RestoreSet[] getAvailableRestoreSets() throws RemoteException {
+ return BackupTransport.this.getAvailableRestoreSets();
+ }
+
+ @Override
+ public long getCurrentRestoreSet() throws RemoteException {
+ return BackupTransport.this.getCurrentRestoreSet();
+ }
+
+ @Override
+ public int startRestore(long token, PackageInfo[] packages) throws RemoteException {
+ return BackupTransport.this.startRestore(token, packages);
+ }
+
+ @Override
+ public RestoreDescription nextRestorePackage() throws RemoteException {
+ return BackupTransport.this.nextRestorePackage();
+ }
+
+ @Override
+ public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException {
+ return BackupTransport.this.getRestoreData(outFd);
+ }
+
+ @Override
+ public void finishRestore() throws RemoteException {
+ BackupTransport.this.finishRestore();
+ }
+
+ @Override
+ public long requestFullBackupTime() throws RemoteException {
+ return BackupTransport.this.requestFullBackupTime();
+ }
+
+ @Override
+ public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket,
+ int flags) throws RemoteException {
+ return BackupTransport.this.performFullBackup(targetPackage, socket, flags);
+ }
+
+ @Override
+ public int checkFullBackupSize(long size) {
+ return BackupTransport.this.checkFullBackupSize(size);
+ }
+
+ @Override
+ public int sendBackupData(int numBytes) throws RemoteException {
+ return BackupTransport.this.sendBackupData(numBytes);
+ }
+
+ @Override
+ public void cancelFullBackup() throws RemoteException {
+ BackupTransport.this.cancelFullBackup();
+ }
+
+ @Override
+ public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup)
+ throws RemoteException {
+ return BackupTransport.this.isAppEligibleForBackup(targetPackage, isFullBackup);
+ }
+
+ @Override
+ public long getBackupQuota(String packageName, boolean isFullBackup) {
+ return BackupTransport.this.getBackupQuota(packageName, isFullBackup);
+ }
+
+ @Override
+ public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
+ return BackupTransport.this.getNextFullRestoreDataChunk(socket);
+ }
+
+ @Override
+ public int abortFullRestore() {
+ return BackupTransport.this.abortFullRestore();
+ }
+ }
+}
diff --git a/android/app/backup/BlobBackupHelper.java b/android/app/backup/BlobBackupHelper.java
new file mode 100644
index 00000000..82d0a94c
--- /dev/null
+++ b/android/app/backup/BlobBackupHelper.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.CRC32;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Utility class for writing BackupHelpers whose underlying data is a
+ * fixed set of byte-array blobs. The helper manages diff detection
+ * and compression on the wire.
+ *
+ * @hide
+ */
+public abstract class BlobBackupHelper implements BackupHelper {
+ private static final String TAG = "BlobBackupHelper";
+ private static final boolean DEBUG = false;
+
+ private final int mCurrentBlobVersion;
+ private final String[] mKeys;
+
+ public BlobBackupHelper(int currentBlobVersion, String... keys) {
+ mCurrentBlobVersion = currentBlobVersion;
+ mKeys = keys;
+ }
+
+ // Client interface
+
+ /**
+ * Generate and return the byte array containing the backup payload describing
+ * the current data state. During a backup operation this method is called once
+ * per key that was supplied to the helper's constructor.
+ *
+ * @return A byte array containing the data blob that the caller wishes to store,
+ * or {@code null} if the current state is empty or undefined.
+ */
+ abstract protected byte[] getBackupPayload(String key);
+
+ /**
+ * Given a byte array that was restored from backup, do whatever is appropriate
+ * to apply that described state in the live system. This method is called once
+ * per key/value payload that was delivered for restore. Typically data is delivered
+ * for restore in lexical order by key, <i>not</i> in the order in which the keys
+ * were supplied in the constructor.
+ *
+ * @param payload The byte array that was passed to {@link #getBackupPayload()}
+ * on the ancestral device.
+ */
+ abstract protected void applyRestoredPayload(String key, byte[] payload);
+
+
+ // Internal implementation
+
+ /*
+ * State on-disk format:
+ * [Int] : overall blob version number
+ * [Int=N] : number of keys represented in the state blob
+ * N* :
+ * [String] key
+ * [Long] blob checksum, calculated after compression
+ */
+ @SuppressWarnings("resource")
+ private ArrayMap<String, Long> readOldState(ParcelFileDescriptor oldStateFd) {
+ final ArrayMap<String, Long> state = new ArrayMap<String, Long>();
+
+ FileInputStream fis = new FileInputStream(oldStateFd.getFileDescriptor());
+ DataInputStream in = new DataInputStream(fis);
+
+ try {
+ int version = in.readInt();
+ if (version <= mCurrentBlobVersion) {
+ final int numKeys = in.readInt();
+ if (DEBUG) {
+ Log.i(TAG, " " + numKeys + " keys in state record");
+ }
+ for (int i = 0; i < numKeys; i++) {
+ String key = in.readUTF();
+ long checksum = in.readLong();
+ if (DEBUG) {
+ Log.i(TAG, " key '" + key + "' checksum is " + checksum);
+ }
+ state.put(key, checksum);
+ }
+ } else {
+ Log.w(TAG, "Prior state from unrecognized version " + version);
+ }
+ } catch (EOFException e) {
+ // Empty file is expected on first backup, so carry on. If the state
+ // is truncated we just treat it the same way.
+ if (DEBUG) {
+ Log.i(TAG, "Hit EOF reading prior state");
+ }
+ state.clear();
+ } catch (Exception e) {
+ Log.e(TAG, "Error examining prior backup state " + e.getMessage());
+ state.clear();
+ }
+
+ return state;
+ }
+
+ /**
+ * New overall state record
+ */
+ private void writeBackupState(ArrayMap<String, Long> state, ParcelFileDescriptor stateFile) {
+ try {
+ FileOutputStream fos = new FileOutputStream(stateFile.getFileDescriptor());
+
+ // We explicitly don't close 'out' because we must not close the backing fd.
+ // The FileOutputStream will not close it implicitly.
+ @SuppressWarnings("resource")
+ DataOutputStream out = new DataOutputStream(fos);
+
+ out.writeInt(mCurrentBlobVersion);
+
+ final int N = (state != null) ? state.size() : 0;
+ out.writeInt(N);
+ for (int i = 0; i < N; i++) {
+ final String key = state.keyAt(i);
+ final long checksum = state.valueAt(i).longValue();
+ if (DEBUG) {
+ Log.i(TAG, " writing key " + key + " checksum = " + checksum);
+ }
+ out.writeUTF(key);
+ out.writeLong(checksum);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to write updated state", e);
+ }
+ }
+
+ // Also versions the deflated blob internally in case we need to revise it
+ private byte[] deflate(byte[] data) {
+ byte[] result = null;
+ if (data != null) {
+ try {
+ ByteArrayOutputStream sink = new ByteArrayOutputStream();
+ DataOutputStream headerOut = new DataOutputStream(sink);
+
+ // write the header directly to the sink ahead of the deflated payload
+ headerOut.writeInt(mCurrentBlobVersion);
+
+ DeflaterOutputStream out = new DeflaterOutputStream(sink);
+ out.write(data);
+ out.close(); // finishes and commits the compression run
+ result = sink.toByteArray();
+ if (DEBUG) {
+ Log.v(TAG, "Deflated " + data.length + " bytes to " + result.length);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to process payload: " + e.getMessage());
+ }
+ }
+ return result;
+ }
+
+ // Returns null if inflation failed
+ private byte[] inflate(byte[] compressedData) {
+ byte[] result = null;
+ if (compressedData != null) {
+ try {
+ ByteArrayInputStream source = new ByteArrayInputStream(compressedData);
+ DataInputStream headerIn = new DataInputStream(source);
+ int version = headerIn.readInt();
+ if (version > mCurrentBlobVersion) {
+ Log.w(TAG, "Saved payload from unrecognized version " + version);
+ return null;
+ }
+
+ InflaterInputStream in = new InflaterInputStream(source);
+ ByteArrayOutputStream inflated = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int nRead;
+ while ((nRead = in.read(buffer)) > 0) {
+ inflated.write(buffer, 0, nRead);
+ }
+ in.close();
+ inflated.flush();
+ result = inflated.toByteArray();
+ if (DEBUG) {
+ Log.v(TAG, "Inflated " + compressedData.length + " bytes to " + result.length);
+ }
+ } catch (IOException e) {
+ // result is still null here
+ Log.w(TAG, "Unable to process restored payload: " + e.getMessage());
+ }
+ }
+ return result;
+ }
+
+ private long checksum(byte[] buffer) {
+ if (buffer != null) {
+ try {
+ CRC32 crc = new CRC32();
+ ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+ byte[] buf = new byte[4096];
+ int nRead = 0;
+ while ((nRead = bis.read(buf)) >= 0) {
+ crc.update(buf, 0, nRead);
+ }
+ return crc.getValue();
+ } catch (Exception e) {
+ // whoops; fall through with an explicitly bogus checksum
+ }
+ }
+ return -1;
+ }
+
+ // BackupHelper interface
+
+ @Override
+ public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data,
+ ParcelFileDescriptor newStateFd) {
+ if (DEBUG) {
+ Log.i(TAG, "Performing backup for " + this.getClass().getName());
+ }
+
+ final ArrayMap<String, Long> oldState = readOldState(oldStateFd);
+ final ArrayMap<String, Long> newState = new ArrayMap<String, Long>();
+
+ try {
+ for (String key : mKeys) {
+ final byte[] payload = deflate(getBackupPayload(key));
+ final long checksum = checksum(payload);
+ if (DEBUG) {
+ Log.i(TAG, "Key " + key + " backup checksum is " + checksum);
+ }
+ newState.put(key, checksum);
+
+ Long oldChecksum = oldState.get(key);
+ if (oldChecksum == null || checksum != oldChecksum.longValue()) {
+ if (DEBUG) {
+ Log.i(TAG, "Checksum has changed from " + oldChecksum + " to " + checksum
+ + " for key " + key + ", writing");
+ }
+ if (payload != null) {
+ data.writeEntityHeader(key, payload.length);
+ data.writeEntityData(payload, payload.length);
+ } else {
+ // state's changed but there's no current payload => delete
+ data.writeEntityHeader(key, -1);
+ }
+ } else {
+ if (DEBUG) {
+ Log.i(TAG, "No change under key " + key + " => not writing");
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to record notification state: " + e.getMessage());
+ newState.clear();
+ } finally {
+ // Always rewrite the state even if nothing changed
+ writeBackupState(newState, newStateFd);
+ }
+ }
+
+ @Override
+ public void restoreEntity(BackupDataInputStream data) {
+ final String key = data.getKey();
+ try {
+ // known key?
+ int which;
+ for (which = 0; which < mKeys.length; which++) {
+ if (key.equals(mKeys[which])) {
+ break;
+ }
+ }
+ if (which >= mKeys.length) {
+ Log.e(TAG, "Unrecognized key " + key + ", ignoring");
+ return;
+ }
+
+ byte[] compressed = new byte[data.size()];
+ data.read(compressed);
+ byte[] payload = inflate(compressed);
+ applyRestoredPayload(key, payload);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception restoring entity " + key + " : " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void writeNewStateDescription(ParcelFileDescriptor newState) {
+ // Just ensure that we do a full backup the first time after a restore
+ if (DEBUG) {
+ Log.i(TAG, "Writing state description after restore");
+ }
+ writeBackupState(null, newState);
+ }
+}
diff --git a/android/app/backup/FileBackupHelper.java b/android/app/backup/FileBackupHelper.java
new file mode 100644
index 00000000..c6a523a9
--- /dev/null
+++ b/android/app/backup/FileBackupHelper.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * A helper class that can be used in conjunction with
+ * {@link android.app.backup.BackupAgentHelper} to manage the backup of a set of
+ * files. Whenever backup is performed, all files changed since the last backup
+ * will be saved in their entirety. When backup first occurs,
+ * every file in the list provided to {@link #FileBackupHelper} will be backed up.
+ * <p>
+ * During restore, if the helper encounters data for a file that was not
+ * specified when the FileBackupHelper object was constructed, that data
+ * will be ignored.
+ * <p class="note"><strong>Note:</strong> This should be
+ * used only with small configuration files, not large binary files.
+ */
+public class FileBackupHelper extends FileBackupHelperBase implements BackupHelper {
+ private static final String TAG = "FileBackupHelper";
+ private static final boolean DEBUG = false;
+
+ Context mContext;
+ File mFilesDir;
+ String[] mFiles;
+
+ /**
+ * Construct a helper to manage backup/restore of entire files within the
+ * application's data directory hierarchy.
+ *
+ * @param context The backup agent's Context object
+ * @param files A list of the files to be backed up or restored.
+ */
+ public FileBackupHelper(Context context, String... files) {
+ super(context);
+
+ mContext = context;
+ mFilesDir = context.getFilesDir();
+ mFiles = files;
+ }
+
+ /**
+ * Based on <code>oldState</code>, determine which of the files from the
+ * application's data directory need to be backed up, write them to the data
+ * stream, and fill in <code>newState</code> with the state as it exists
+ * now. When <code>oldState</code> is <code>null</code>, all the files will
+ * be backed up.
+ * <p>
+ * This should only be called directly from within the {@link BackupAgentHelper}
+ * implementation. See
+ * {@link android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)}
+ * for a description of parameter meanings.
+ */
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) {
+ // file names
+ String[] files = mFiles;
+ File base = mContext.getFilesDir();
+ final int N = files.length;
+ String[] fullPaths = new String[N];
+ for (int i=0; i<N; i++) {
+ fullPaths[i] = (new File(base, files[i])).getAbsolutePath();
+ }
+
+ // go
+ performBackup_checked(oldState, data, newState, fullPaths, files);
+ }
+
+ /**
+ * Restore one record [representing a single file] from the restore dataset.
+ * <p>
+ * This should only be called directly from within the {@link BackupAgentHelper}
+ * implementation.
+ */
+ public void restoreEntity(BackupDataInputStream data) {
+ if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size());
+ String key = data.getKey();
+ if (isKeyInList(key, mFiles)) {
+ File f = new File(mFilesDir, key);
+ writeFile(f, data);
+ }
+ }
+}
+
diff --git a/android/app/backup/FileBackupHelperBase.java b/android/app/backup/FileBackupHelperBase.java
new file mode 100644
index 00000000..4ed5197a
--- /dev/null
+++ b/android/app/backup/FileBackupHelperBase.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileDescriptor;
+
+/**
+ * Base class for the {@link android.app.backup.FileBackupHelper} implementation.
+ */
+class FileBackupHelperBase {
+ private static final String TAG = "FileBackupHelperBase";
+
+ long mPtr;
+ Context mContext;
+ boolean mExceptionLogged;
+
+ FileBackupHelperBase(Context context) {
+ mPtr = ctor();
+ mContext = context;
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ dtor(mPtr);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Check the parameters so the native code doesn't have to throw all the exceptions
+ * since it's easier to do that from Java.
+ */
+ static void performBackup_checked(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState, String[] files, String[] keys) {
+ if (files.length == 0) {
+ return;
+ }
+ // files must be all absolute paths
+ for (String f: files) {
+ if (f.charAt(0) != '/') {
+ throw new RuntimeException("files must have all absolute paths: " + f);
+ }
+ }
+ // the length of files and keys must be the same
+ if (files.length != keys.length) {
+ throw new RuntimeException("files.length=" + files.length
+ + " keys.length=" + keys.length);
+ }
+ // oldStateFd can be null
+ FileDescriptor oldStateFd = oldState != null ? oldState.getFileDescriptor() : null;
+ FileDescriptor newStateFd = newState.getFileDescriptor();
+ if (newStateFd == null) {
+ throw new NullPointerException();
+ }
+
+ int err = performBackup_native(oldStateFd, data.mBackupWriter, newStateFd, files, keys);
+
+ if (err != 0) {
+ // TODO: more here
+ throw new RuntimeException("Backup failed 0x" + Integer.toHexString(err));
+ }
+ }
+
+ boolean writeFile(File f, BackupDataInputStream in) {
+ int result = -1;
+
+ // Create the enclosing directory.
+ File parent = f.getParentFile();
+ parent.mkdirs();
+
+ result = writeFile_native(mPtr, f.getAbsolutePath(), in.mData.mBackupReader);
+ if (result != 0) {
+ // Bail on this entity. Only log one failure per helper object.
+ if (!mExceptionLogged) {
+ Log.e(TAG, "Failed restoring file '" + f + "' for app '"
+ + mContext.getPackageName() + "\' result=0x"
+ + Integer.toHexString(result));
+ mExceptionLogged = true;
+ }
+ }
+ return (result == 0);
+ }
+
+ public void writeNewStateDescription(ParcelFileDescriptor fd) {
+ int result = writeSnapshot_native(mPtr, fd.getFileDescriptor());
+ // TODO: Do something with the error.
+ }
+
+ boolean isKeyInList(String key, String[] list) {
+ for (String s: list) {
+ if (s.equals(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static native long ctor();
+ private static native void dtor(long ptr);
+
+ native private static int performBackup_native(FileDescriptor oldState,
+ long data, FileDescriptor newState, String[] files, String[] keys);
+ private static native int writeFile_native(long ptr, String filename, long backupReader);
+ private static native int writeSnapshot_native(long ptr, FileDescriptor fd);
+}
+
+
diff --git a/android/app/backup/FullBackup.java b/android/app/backup/FullBackup.java
new file mode 100644
index 00000000..a5dd5bd3
--- /dev/null
+++ b/android/app/backup/FullBackup.java
@@ -0,0 +1,665 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.XmlResourceParser;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Global constant definitions et cetera related to the full-backup-to-fd
+ * binary format. Nothing in this namespace is part of any API; it's all
+ * hidden details of the current implementation gathered into one location.
+ *
+ * @hide
+ */
+public class FullBackup {
+ static final String TAG = "FullBackup";
+ /** Enable this log tag to get verbose information while parsing the client xml. */
+ static final String TAG_XML_PARSER = "BackupXmlParserLogging";
+
+ public static final String APK_TREE_TOKEN = "a";
+ public static final String OBB_TREE_TOKEN = "obb";
+ public static final String KEY_VALUE_DATA_TOKEN = "k";
+
+ public static final String ROOT_TREE_TOKEN = "r";
+ public static final String FILES_TREE_TOKEN = "f";
+ public static final String NO_BACKUP_TREE_TOKEN = "nb";
+ public static final String DATABASE_TREE_TOKEN = "db";
+ public static final String SHAREDPREFS_TREE_TOKEN = "sp";
+ public static final String CACHE_TREE_TOKEN = "c";
+
+ public static final String DEVICE_ROOT_TREE_TOKEN = "d_r";
+ public static final String DEVICE_FILES_TREE_TOKEN = "d_f";
+ public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb";
+ public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db";
+ public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp";
+ public static final String DEVICE_CACHE_TREE_TOKEN = "d_c";
+
+ public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";
+ public static final String SHARED_STORAGE_TOKEN = "shared";
+
+ public static final String APPS_PREFIX = "apps/";
+ public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
+
+ public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
+ public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
+ public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";
+
+ /**
+ * @hide
+ */
+ static public native int backupToTar(String packageName, String domain,
+ String linkdomain, String rootpath, String path, FullBackupDataOutput output);
+
+ private static final Map<String, BackupScheme> kPackageBackupSchemeMap =
+ new ArrayMap<String, BackupScheme>();
+
+ static synchronized BackupScheme getBackupScheme(Context context) {
+ BackupScheme backupSchemeForPackage =
+ kPackageBackupSchemeMap.get(context.getPackageName());
+ if (backupSchemeForPackage == null) {
+ backupSchemeForPackage = new BackupScheme(context);
+ kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage);
+ }
+ return backupSchemeForPackage;
+ }
+
+ public static BackupScheme getBackupSchemeForTest(Context context) {
+ BackupScheme testing = new BackupScheme(context);
+ testing.mExcludes = new ArraySet();
+ testing.mIncludes = new ArrayMap();
+ return testing;
+ }
+
+
+ /**
+ * Copy data from a socket to the given File location on permanent storage. The
+ * modification time and access mode of the resulting file will be set if desired,
+ * although group/all rwx modes will be stripped: the restored file will not be
+ * accessible from outside the target application even if the original file was.
+ * If the {@code type} parameter indicates that the result should be a directory,
+ * the socket parameter may be {@code null}; even if it is valid, no data will be
+ * read from it in this case.
+ * <p>
+ * If the {@code mode} argument is negative, then the resulting output file will not
+ * have its access mode or last modification time reset as part of this operation.
+ *
+ * @param data Socket supplying the data to be copied to the output file. If the
+ * output is a directory, this may be {@code null}.
+ * @param size Number of bytes of data to copy from the socket to the file. At least
+ * this much data must be available through the {@code data} parameter.
+ * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
+ * or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
+ * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
+ * the output file or directory. group/all rwx modes are stripped even if set
+ * in this parameter. If this parameter is negative then neither
+ * the mode nor the mtime values will be applied to the restored file.
+ * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
+ * last modification time of the output file. if the {@code mode} parameter is
+ * negative then this parameter will be ignored.
+ * @param outFile Location within the filesystem to place the data. This must point
+ * to a location that is writeable by the caller, preferably using an absolute path.
+ * @throws IOException
+ */
+ static public void restoreFile(ParcelFileDescriptor data,
+ long size, int type, long mode, long mtime, File outFile) throws IOException {
+ if (type == BackupAgent.TYPE_DIRECTORY) {
+ // Canonically a directory has no associated content, so we don't need to read
+ // anything from the pipe in this case. Just create the directory here and
+ // drop down to the final metadata adjustment.
+ if (outFile != null) outFile.mkdirs();
+ } else {
+ FileOutputStream out = null;
+
+ // Pull the data from the pipe, copying it to the output file, until we're done
+ try {
+ if (outFile != null) {
+ File parent = outFile.getParentFile();
+ if (!parent.exists()) {
+ // in practice this will only be for the default semantic directories,
+ // and using the default mode for those is appropriate.
+ // This can also happen for the case where a parent directory has been
+ // excluded, but a file within that directory has been included.
+ parent.mkdirs();
+ }
+ out = new FileOutputStream(outFile);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
+ }
+
+ byte[] buffer = new byte[32 * 1024];
+ final long origSize = size;
+ FileInputStream in = new FileInputStream(data.getFileDescriptor());
+ while (size > 0) {
+ int toRead = (size > buffer.length) ? buffer.length : (int)size;
+ int got = in.read(buffer, 0, toRead);
+ if (got <= 0) {
+ Log.w(TAG, "Incomplete read: expected " + size + " but got "
+ + (origSize - size));
+ break;
+ }
+ if (out != null) {
+ try {
+ out.write(buffer, 0, got);
+ } catch (IOException e) {
+ // Problem writing to the file. Quit copying data and delete
+ // the file, but of course keep consuming the input stream.
+ Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
+ out.close();
+ out = null;
+ outFile.delete();
+ }
+ }
+ size -= got;
+ }
+ if (out != null) out.close();
+ }
+
+ // Now twiddle the state to match the backup, assuming all went well
+ if (mode >= 0 && outFile != null) {
+ try {
+ // explicitly prevent emplacement of files accessible by outside apps
+ mode &= 0700;
+ Os.chmod(outFile.getPath(), (int)mode);
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+ outFile.setLastModified(mtime);
+ }
+ }
+
+ @VisibleForTesting
+ public static class BackupScheme {
+ private final File FILES_DIR;
+ private final File DATABASE_DIR;
+ private final File ROOT_DIR;
+ private final File SHAREDPREF_DIR;
+ private final File CACHE_DIR;
+ private final File NOBACKUP_DIR;
+
+ private final File DEVICE_FILES_DIR;
+ private final File DEVICE_DATABASE_DIR;
+ private final File DEVICE_ROOT_DIR;
+ private final File DEVICE_SHAREDPREF_DIR;
+ private final File DEVICE_CACHE_DIR;
+ private final File DEVICE_NOBACKUP_DIR;
+
+ private final File EXTERNAL_DIR;
+
+ final int mFullBackupContent;
+ final PackageManager mPackageManager;
+ final StorageManager mStorageManager;
+ final String mPackageName;
+
+ // lazy initialized, only when needed
+ private StorageVolume[] mVolumes = null;
+
+ /**
+ * Parse out the semantic domains into the correct physical location.
+ */
+ String tokenToDirectoryPath(String domainToken) {
+ try {
+ if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
+ return FILES_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
+ return DATABASE_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
+ return ROOT_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
+ return SHAREDPREF_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
+ return CACHE_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
+ return NOBACKUP_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) {
+ return DEVICE_FILES_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) {
+ return DEVICE_DATABASE_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) {
+ return DEVICE_ROOT_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) {
+ return DEVICE_SHAREDPREF_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) {
+ return DEVICE_CACHE_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) {
+ return DEVICE_NOBACKUP_DIR.getCanonicalPath();
+ } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
+ if (EXTERNAL_DIR != null) {
+ return EXTERNAL_DIR.getCanonicalPath();
+ } else {
+ return null;
+ }
+ } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
+ return sharedDomainToPath(domainToken);
+ }
+ // Not a supported location
+ Log.i(TAG, "Unrecognized domain " + domainToken);
+ return null;
+ } catch (Exception e) {
+ Log.i(TAG, "Error reading directory for domain: " + domainToken);
+ return null;
+ }
+
+ }
+
+ private String sharedDomainToPath(String domain) throws IOException {
+ // already known to start with SHARED_PREFIX, so we just look after that
+ final String volume = domain.substring(FullBackup.SHARED_PREFIX.length());
+ final StorageVolume[] volumes = getVolumeList();
+ final int volNum = Integer.parseInt(volume);
+ if (volNum < mVolumes.length) {
+ return volumes[volNum].getPathFile().getCanonicalPath();
+ }
+ return null;
+ }
+
+ private StorageVolume[] getVolumeList() {
+ if (mStorageManager != null) {
+ if (mVolumes == null) {
+ mVolumes = mStorageManager.getVolumeList();
+ }
+ } else {
+ Log.e(TAG, "Unable to access Storage Manager");
+ }
+ return mVolumes;
+ }
+
+ /**
+ * A map of domain -> list of canonical file names in that domain that are to be included.
+ * We keep track of the domain so that we can go through the file system in order later on.
+ */
+ Map<String, Set<String>> mIncludes;
+ /**e
+ * List that will be populated with the canonical names of each file or directory that is
+ * to be excluded.
+ */
+ ArraySet<String> mExcludes;
+
+ BackupScheme(Context context) {
+ mFullBackupContent = context.getApplicationInfo().fullBackupContent;
+ mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
+ mPackageManager = context.getPackageManager();
+ mPackageName = context.getPackageName();
+
+ // System apps have control over where their default storage context
+ // is pointed, so we're always explicit when building paths.
+ final Context ceContext = context.createCredentialProtectedStorageContext();
+ FILES_DIR = ceContext.getFilesDir();
+ DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile();
+ ROOT_DIR = ceContext.getDataDir();
+ SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile();
+ CACHE_DIR = ceContext.getCacheDir();
+ NOBACKUP_DIR = ceContext.getNoBackupFilesDir();
+
+ final Context deContext = context.createDeviceProtectedStorageContext();
+ DEVICE_FILES_DIR = deContext.getFilesDir();
+ DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile();
+ DEVICE_ROOT_DIR = deContext.getDataDir();
+ DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile();
+ DEVICE_CACHE_DIR = deContext.getCacheDir();
+ DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir();
+
+ if (android.os.Process.myUid() != Process.SYSTEM_UID) {
+ EXTERNAL_DIR = context.getExternalFilesDir(null);
+ } else {
+ EXTERNAL_DIR = null;
+ }
+ }
+
+ boolean isFullBackupContentEnabled() {
+ if (mFullBackupContent < 0) {
+ // android:fullBackupContent="false", bail.
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return A mapping of domain -> canonical paths within that domain. Each of these paths
+ * specifies a file that the client has explicitly included in their backup set. If this
+ * map is empty we will back up the entire data directory (including managed external
+ * storage).
+ */
+ public synchronized Map<String, Set<String>> maybeParseAndGetCanonicalIncludePaths()
+ throws IOException, XmlPullParserException {
+ if (mIncludes == null) {
+ maybeParseBackupSchemeLocked();
+ }
+ return mIncludes;
+ }
+
+ /**
+ * @return A set of canonical paths that are to be excluded from the backup/restore set.
+ */
+ public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths()
+ throws IOException, XmlPullParserException {
+ if (mExcludes == null) {
+ maybeParseBackupSchemeLocked();
+ }
+ return mExcludes;
+ }
+
+ private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
+ // This not being null is how we know that we've tried to parse the xml already.
+ mIncludes = new ArrayMap<String, Set<String>>();
+ mExcludes = new ArraySet<String>();
+
+ if (mFullBackupContent == 0) {
+ // android:fullBackupContent="true" which means that we'll do everything.
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
+ }
+ } else {
+ // android:fullBackupContent="@xml/some_resource".
+ if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(FullBackup.TAG_XML_PARSER,
+ "android:fullBackupContent - found xml resource");
+ }
+ XmlResourceParser parser = null;
+ try {
+ parser = mPackageManager
+ .getResourcesForApplication(mPackageName)
+ .getXml(mFullBackupContent);
+ parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Throw it as an IOException
+ throw new IOException(e);
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
+ Set<String> excludes,
+ Map<String, Set<String>> includes)
+ throws IOException, XmlPullParserException {
+ int event = parser.getEventType(); // START_DOCUMENT
+ while (event != XmlPullParser.START_TAG) {
+ event = parser.next();
+ }
+
+ if (!"full-backup-content".equals(parser.getName())) {
+ throw new XmlPullParserException("Xml file didn't start with correct tag" +
+ " (<full-backup-content>). Found \"" + parser.getName() + "\"");
+ }
+
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "\n");
+ Log.v(TAG_XML_PARSER, "====================================================");
+ Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource.");
+ Log.v(TAG_XML_PARSER, "====================================================");
+ Log.v(TAG_XML_PARSER, "");
+ }
+
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ switch (event) {
+ case XmlPullParser.START_TAG:
+ validateInnerTagContents(parser);
+ final String domainFromXml = parser.getAttributeValue(null, "domain");
+ final File domainDirectory =
+ getDirectoryForCriteriaDomain(domainFromXml);
+ if (domainDirectory == null) {
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
+ + "domain=\"" + domainFromXml + "\" invalid; skipping");
+ }
+ break;
+ }
+ final File canonicalFile =
+ extractCanonicalFile(domainDirectory,
+ parser.getAttributeValue(null, "path"));
+ if (canonicalFile == null) {
+ break;
+ }
+
+ Set<String> activeSet = parseCurrentTagForDomain(
+ parser, excludes, includes, domainFromXml);
+ activeSet.add(canonicalFile.getCanonicalPath());
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
+ + " for domain \"" + domainFromXml + "\"");
+ }
+
+ // Special case journal files (not dirs) for sqlite database. frowny-face.
+ // Note that for a restore, the file is never a directory (b/c it doesn't
+ // exist). We have no way of knowing a priori whether or not to expect a
+ // dir, so we add the -journal anyway to be safe.
+ if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
+ final String canonicalJournalPath =
+ canonicalFile.getCanonicalPath() + "-journal";
+ activeSet.add(canonicalJournalPath);
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...automatically generated "
+ + canonicalJournalPath + ". Ignore if nonexistent.");
+ }
+ final String canonicalWalPath =
+ canonicalFile.getCanonicalPath() + "-wal";
+ activeSet.add(canonicalWalPath);
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...automatically generated "
+ + canonicalWalPath + ". Ignore if nonexistent.");
+ }
+ }
+
+ // Special case for sharedpref files (not dirs) also add ".xml" suffix file.
+ if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() &&
+ !canonicalFile.getCanonicalPath().endsWith(".xml")) {
+ final String canonicalXmlPath =
+ canonicalFile.getCanonicalPath() + ".xml";
+ activeSet.add(canonicalXmlPath);
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...automatically generated "
+ + canonicalXmlPath + ". Ignore if nonexistent.");
+ }
+ }
+ }
+ }
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "\n");
+ Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
+ Log.v(TAG_XML_PARSER, "Final tally.");
+ Log.v(TAG_XML_PARSER, "Includes:");
+ if (includes.isEmpty()) {
+ Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app"
+ + " data minus excludes)");
+ } else {
+ for (Map.Entry<String, Set<String>> entry : includes.entrySet()) {
+ Log.v(TAG_XML_PARSER, " domain=" + entry.getKey());
+ for (String includeData : entry.getValue()) {
+ Log.v(TAG_XML_PARSER, " " + includeData);
+ }
+ }
+ }
+
+ Log.v(TAG_XML_PARSER, "Excludes:");
+ if (excludes.isEmpty()) {
+ Log.v(TAG_XML_PARSER, " ...nothing to exclude.");
+ } else {
+ for (String excludeData : excludes) {
+ Log.v(TAG_XML_PARSER, " " + excludeData);
+ }
+ }
+
+ Log.v(TAG_XML_PARSER, " ");
+ Log.v(TAG_XML_PARSER, "====================================================");
+ Log.v(TAG_XML_PARSER, "\n");
+ }
+ }
+
+ private Set<String> parseCurrentTagForDomain(XmlPullParser parser,
+ Set<String> excludes,
+ Map<String, Set<String>> includes,
+ String domain)
+ throws XmlPullParserException {
+ if ("include".equals(parser.getName())) {
+ final String domainToken = getTokenForXmlDomain(domain);
+ Set<String> includeSet = includes.get(domainToken);
+ if (includeSet == null) {
+ includeSet = new ArraySet<String>();
+ includes.put(domainToken, includeSet);
+ }
+ return includeSet;
+ } else if ("exclude".equals(parser.getName())) {
+ return excludes;
+ } else {
+ // Unrecognised tag => hard failure.
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
+ + parser.getName() + "\"; aborting operation.");
+ }
+ throw new XmlPullParserException("Unrecognised tag in backup" +
+ " criteria xml (" + parser.getName() + ")");
+ }
+ }
+
+ /**
+ * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
+ * BackupAgent internal data token.
+ * @return null if the xml domain was invalid.
+ */
+ private String getTokenForXmlDomain(String xmlDomain) {
+ if ("root".equals(xmlDomain)) {
+ return FullBackup.ROOT_TREE_TOKEN;
+ } else if ("file".equals(xmlDomain)) {
+ return FullBackup.FILES_TREE_TOKEN;
+ } else if ("database".equals(xmlDomain)) {
+ return FullBackup.DATABASE_TREE_TOKEN;
+ } else if ("sharedpref".equals(xmlDomain)) {
+ return FullBackup.SHAREDPREFS_TREE_TOKEN;
+ } else if ("device_root".equals(xmlDomain)) {
+ return FullBackup.DEVICE_ROOT_TREE_TOKEN;
+ } else if ("device_file".equals(xmlDomain)) {
+ return FullBackup.DEVICE_FILES_TREE_TOKEN;
+ } else if ("device_database".equals(xmlDomain)) {
+ return FullBackup.DEVICE_DATABASE_TREE_TOKEN;
+ } else if ("device_sharedpref".equals(xmlDomain)) {
+ return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
+ } else if ("external".equals(xmlDomain)) {
+ return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ *
+ * @param domain Directory where the specified file should exist. Not null.
+ * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may be
+ * null.
+ * @return The canonical path of the file specified or null if no such file exists.
+ */
+ private File extractCanonicalFile(File domain, String filePathFromXml) {
+ if (filePathFromXml == null) {
+ // Allow things like <include domain="sharedpref"/>
+ filePathFromXml = "";
+ }
+ if (filePathFromXml.contains("..")) {
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
+ + "\", but the \"..\" path is not permitted; skipping.");
+ }
+ return null;
+ }
+ if (filePathFromXml.contains("//")) {
+ if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
+ Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
+ + "\", which contains the invalid \"//\" sequence; skipping.");
+ }
+ return null;
+ }
+ return new File(domain, filePathFromXml);
+ }
+
+ /**
+ * @param domain parsed from xml. Not sanitised before calling this function so may be null.
+ * @return The directory relevant to the domain specified.
+ */
+ private File getDirectoryForCriteriaDomain(String domain) {
+ if (TextUtils.isEmpty(domain)) {
+ return null;
+ }
+ if ("file".equals(domain)) {
+ return FILES_DIR;
+ } else if ("database".equals(domain)) {
+ return DATABASE_DIR;
+ } else if ("root".equals(domain)) {
+ return ROOT_DIR;
+ } else if ("sharedpref".equals(domain)) {
+ return SHAREDPREF_DIR;
+ } else if ("device_file".equals(domain)) {
+ return DEVICE_FILES_DIR;
+ } else if ("device_database".equals(domain)) {
+ return DEVICE_DATABASE_DIR;
+ } else if ("device_root".equals(domain)) {
+ return DEVICE_ROOT_DIR;
+ } else if ("device_sharedpref".equals(domain)) {
+ return DEVICE_SHAREDPREF_DIR;
+ } else if ("external".equals(domain)) {
+ return EXTERNAL_DIR;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Let's be strict about the type of xml the client can write. If we see anything untoward,
+ * throw an XmlPullParserException.
+ */
+ private void validateInnerTagContents(XmlPullParser parser)
+ throws XmlPullParserException {
+ if (parser.getAttributeCount() > 2) {
+ throw new XmlPullParserException("At most 2 tag attributes allowed for \""
+ + parser.getName() + "\" tag (\"domain\" & \"path\".");
+ }
+ if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) {
+ throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
+ " \"<exclude/>. You provided \"" + parser.getName() + "\"");
+ }
+ }
+ }
+}
diff --git a/android/app/backup/FullBackupAgent.java b/android/app/backup/FullBackupAgent.java
new file mode 100644
index 00000000..faea76ae
--- /dev/null
+++ b/android/app/backup/FullBackupAgent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+import java.io.IOException;
+
+/**
+ * Simple concrete class that merely provides the default BackupAgent full backup/restore
+ * implementations for applications that do not supply their own.
+ *
+ * {@hide}
+ */
+
+public class FullBackupAgent extends BackupAgent {
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ // Doesn't do incremental backup/restore
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ // Doesn't do incremental backup/restore
+ }
+}
diff --git a/android/app/backup/FullBackupDataOutput.java b/android/app/backup/FullBackupDataOutput.java
new file mode 100644
index 00000000..5deedd03
--- /dev/null
+++ b/android/app/backup/FullBackupDataOutput.java
@@ -0,0 +1,56 @@
+package android.app.backup;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Provides the interface through which a {@link BackupAgent} writes entire files
+ * to a full backup data set, via its {@link BackupAgent#onFullBackup(FullBackupDataOutput)}
+ * method.
+ */
+public class FullBackupDataOutput {
+ // Currently a name-scoping shim around BackupDataOutput
+ private final BackupDataOutput mData;
+ private final long mQuota;
+ private long mSize;
+
+ /**
+ * Returns the quota in bytes for the application's current backup operation. The
+ * value can vary for each operation.
+ *
+ * @see BackupDataOutput#getQuota()
+ */
+ public long getQuota() {
+ return mQuota;
+ }
+
+ /** @hide - used only in measure operation */
+ public FullBackupDataOutput(long quota) {
+ mData = null;
+ mQuota = quota;
+ mSize = 0;
+ }
+
+ /** @hide */
+ public FullBackupDataOutput(ParcelFileDescriptor fd, long quota) {
+ mData = new BackupDataOutput(fd.getFileDescriptor(), quota);
+ mQuota = quota;
+ }
+
+ /** @hide - used only internally to the backup manager service's stream construction */
+ public FullBackupDataOutput(ParcelFileDescriptor fd) {
+ this(fd, -1);
+ }
+
+ /** @hide */
+ public BackupDataOutput getData() { return mData; }
+
+ /** @hide - used for measurement pass */
+ public void addSize(long size) {
+ if (size > 0) {
+ mSize += size;
+ }
+ }
+
+ /** @hide - used for measurement pass */
+ public long getSize() { return mSize; }
+}
diff --git a/android/app/backup/RestoreDescription.java b/android/app/backup/RestoreDescription.java
new file mode 100644
index 00000000..0250326e
--- /dev/null
+++ b/android/app/backup/RestoreDescription.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Description of the available restore data for a given package. Returned by a
+ * BackupTransport in response to a request about the next available restorable
+ * package.
+ *
+ * @see BackupTransport#nextRestorePackage()
+ *
+ * @hide
+ */
+@SystemApi
+public class RestoreDescription implements Parcelable {
+ private final String mPackageName;
+ private final int mDataType;
+
+ private static final String NO_MORE_PACKAGES_SENTINEL = "NO_MORE_PACKAGES";
+
+ /**
+ * Return this constant RestoreDescription from BackupTransport.nextRestorePackage()
+ * to indicate that no more package data is available in the current restore operation.
+ */
+ public static final RestoreDescription NO_MORE_PACKAGES =
+ new RestoreDescription(NO_MORE_PACKAGES_SENTINEL, 0);
+
+ // ---------------------------------------
+ // Data type identifiers
+
+ /** This package's restore data is an original-style key/value dataset */
+ public static final int TYPE_KEY_VALUE = 1;
+
+ /** This package's restore data is a tarball-type full data stream */
+ public static final int TYPE_FULL_STREAM = 2;
+
+ @Override
+ public String toString() {
+ return "RestoreDescription{" + mPackageName + " : "
+ + ((mDataType == TYPE_KEY_VALUE) ? "KEY_VALUE" : "STREAM")
+ + '}';
+ }
+
+ // ---------------------------------------
+ // API
+
+ public RestoreDescription(String packageName, int dataType) {
+ mPackageName = packageName;
+ mDataType = dataType;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public int getDataType() {
+ return mDataType;
+ }
+
+ // ---------------------------------------
+ // Parcelable implementation - not used by transport
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mPackageName);
+ out.writeInt(mDataType);
+ }
+
+ public static final Parcelable.Creator<RestoreDescription> CREATOR
+ = new Parcelable.Creator<RestoreDescription>() {
+ public RestoreDescription createFromParcel(Parcel in) {
+ final RestoreDescription unparceled = new RestoreDescription(in);
+ return (NO_MORE_PACKAGES_SENTINEL.equals(unparceled.mPackageName))
+ ? NO_MORE_PACKAGES
+ : unparceled;
+ }
+
+ public RestoreDescription[] newArray(int size) {
+ return new RestoreDescription[size];
+ }
+ };
+
+ private RestoreDescription(Parcel in) {
+ mPackageName = in.readString();
+ mDataType = in.readInt();
+ }
+}
diff --git a/android/app/backup/RestoreObserver.java b/android/app/backup/RestoreObserver.java
new file mode 100644
index 00000000..b4a23d09
--- /dev/null
+++ b/android/app/backup/RestoreObserver.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import java.lang.String;
+
+import android.annotation.SystemApi;
+import android.app.backup.RestoreSet;
+
+/**
+ * Callback class for receiving progress reports during a restore operation. These
+ * methods will all be called on your application's main thread.
+ */
+public abstract class RestoreObserver {
+ /**
+ * Supply a list of the restore datasets available from the current transport. This
+ * method is invoked as a callback following the application's use of the
+ * {@link android.app.backup.IRestoreSession.getAvailableRestoreSets} method.
+ *
+ * @param result An array of {@link android.app.backup.RestoreSet RestoreSet} objects
+ * describing all of the available datasets that are candidates for restoring to
+ * the current device. If no applicable datasets exist, {@code result} will be
+ * {@code null}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void restoreSetsAvailable(RestoreSet[] result) {
+ }
+
+ /**
+ * The restore operation has begun.
+ *
+ * @param numPackages The total number of packages being processed in
+ * this restore operation.
+ */
+ public void restoreStarting(int numPackages) {
+ }
+
+ /**
+ * An indication of which package is being restored currently, out of the
+ * total number provided in the {@link #restoreStarting(int)} callback. This method
+ * is not guaranteed to be called: if the transport is unable to obtain
+ * data for one or more of the requested packages, no onUpdate() call will
+ * occur for those packages.
+ *
+ * @param nowBeingRestored The index, between 1 and the numPackages parameter
+ * to the {@link #restoreStarting(int)} callback, of the package now being
+ * restored. This may be non-monotonic; it is intended purely as a rough
+ * indication of the backup manager's progress through the overall restore process.
+ * @param currentPackage The name of the package now being restored.
+ */
+ public void onUpdate(int nowBeingRestored, String currentPackage) {
+ }
+
+ /**
+ * The restore process has completed. This method will always be called,
+ * even if no individual package restore operations were attempted.
+ *
+ * @param error Zero on success; a nonzero error code if the restore operation
+ * as a whole failed.
+ */
+ public void restoreFinished(int error) {
+ }
+}
diff --git a/android/app/backup/RestoreSession.java b/android/app/backup/RestoreSession.java
new file mode 100644
index 00000000..69d964da
--- /dev/null
+++ b/android/app/backup/RestoreSession.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.app.backup.RestoreObserver;
+import android.app.backup.RestoreSet;
+import android.app.backup.IRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Interface for managing a restore session.
+ * @hide
+ */
+@SystemApi
+public class RestoreSession {
+ static final String TAG = "RestoreSession";
+
+ final Context mContext;
+ IRestoreSession mBinder;
+ RestoreObserverWrapper mObserver = null;
+
+ /**
+ * Ask the current transport what the available restore sets are.
+ *
+ * @param observer a RestoreObserver object whose restoreSetsAvailable() method will
+ * be called on the application's main thread in order to supply the results of
+ * the restore set lookup by the backup transport. This parameter must not be
+ * null.
+ * @param monitor a BackupManagerMonitor object will supply data about important events.
+ * @return Zero on success, nonzero on error. The observer's restoreSetsAvailable()
+ * method will only be called if this method returned zero.
+ */
+ public int getAvailableRestoreSets(RestoreObserver observer, BackupManagerMonitor monitor) {
+ int err = -1;
+ RestoreObserverWrapper obsWrapper = new RestoreObserverWrapper(mContext, observer);
+ BackupManagerMonitorWrapper monitorWrapper = monitor == null
+ ? null
+ : new BackupManagerMonitorWrapper(monitor);
+ try {
+ err = mBinder.getAvailableRestoreSets(obsWrapper, monitorWrapper);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Can't contact server to get available sets");
+ }
+ return err;
+ }
+
+ /**
+ * Ask the current transport what the available restore sets are.
+ *
+ * @param observer a RestoreObserver object whose restoreSetsAvailable() method will
+ * be called on the application's main thread in order to supply the results of
+ * the restore set lookup by the backup transport. This parameter must not be
+ * null.
+ * @return Zero on success, nonzero on error. The observer's restoreSetsAvailable()
+ * method will only be called if this method returned zero.
+ */
+ public int getAvailableRestoreSets(RestoreObserver observer) {
+ return getAvailableRestoreSets(observer, null);
+ }
+
+ /**
+ * Restore the given set onto the device, replacing the current data of any app
+ * contained in the restore set with the data previously backed up.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ *
+ * @return Zero on success; nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param token The token from {@link #getAvailableRestoreSets()} corresponding to
+ * the restore set that should be used.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ * @param monitor If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ */
+ public int restoreAll(long token, RestoreObserver observer, BackupManagerMonitor monitor) {
+ int err = -1;
+ if (mObserver != null) {
+ Log.d(TAG, "restoreAll() called during active restore");
+ return -1;
+ }
+ mObserver = new RestoreObserverWrapper(mContext, observer);
+ BackupManagerMonitorWrapper monitorWrapper = monitor == null
+ ? null
+ : new BackupManagerMonitorWrapper(monitor);
+ try {
+ err = mBinder.restoreAll(token, mObserver, monitorWrapper);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Can't contact server to restore");
+ }
+ return err;
+ }
+
+ /**
+ * Restore the given set onto the device, replacing the current data of any app
+ * contained in the restore set with the data previously backed up.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ *
+ * @return Zero on success; nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param token The token from {@link #getAvailableRestoreSets()} corresponding to
+ * the restore set that should be used.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ */
+ public int restoreAll(long token, RestoreObserver observer) {
+ return restoreAll(token, observer, null);
+ }
+
+ /**
+ * Restore select packages from the given set onto the device, replacing the
+ * current data of any app contained in the set with the data previously
+ * backed up.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ *
+ * @return Zero on success, nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param token The token from {@link getAvailableRestoreSets()} corresponding to
+ * the restore set that should be used.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ * @param monitor If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ * @param packages The set of packages for which to attempt a restore. Regardless of
+ * the contents of the actual back-end dataset named by {@code token}, only
+ * applications mentioned in this list will have their data restored.
+ *
+ * @hide
+ */
+ public int restoreSome(long token, RestoreObserver observer, BackupManagerMonitor monitor,
+ String[] packages) {
+ int err = -1;
+ if (mObserver != null) {
+ Log.d(TAG, "restoreAll() called during active restore");
+ return -1;
+ }
+ mObserver = new RestoreObserverWrapper(mContext, observer);
+ BackupManagerMonitorWrapper monitorWrapper = monitor == null
+ ? null
+ : new BackupManagerMonitorWrapper(monitor);
+ try {
+ err = mBinder.restoreSome(token, mObserver, monitorWrapper, packages);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Can't contact server to restore packages");
+ }
+ return err;
+ }
+
+ /**
+ * Restore select packages from the given set onto the device, replacing the
+ * current data of any app contained in the set with the data previously
+ * backed up.
+ *
+ * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+ *
+ * @return Zero on success, nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param token The token from {@link getAvailableRestoreSets()} corresponding to
+ * the restore set that should be used.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ * @param packages The set of packages for which to attempt a restore. Regardless of
+ * the contents of the actual back-end dataset named by {@code token}, only
+ * applications mentioned in this list will have their data restored.
+ *
+ * @hide
+ */
+ public int restoreSome(long token, RestoreObserver observer, String[] packages) {
+ return restoreSome(token, observer, null, packages);
+ }
+
+ /**
+ * Restore a single application from backup. The data will be restored from the
+ * current backup dataset if the given package has stored data there, or from
+ * the dataset used during the last full device setup operation if the current
+ * backup dataset has no matching data. If no backup data exists for this package
+ * in either source, a nonzero value will be returned.
+ *
+ * @return Zero on success; nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param packageName The name of the package whose data to restore. If this is
+ * not the name of the caller's own package, then the android.permission.BACKUP
+ * permission must be held.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ *
+ * @param monitor If non-null, this binder points to an object that will receive
+ * event callbacks during the restore operation.
+ */
+ public int restorePackage(String packageName, RestoreObserver observer,
+ BackupManagerMonitor monitor) {
+ int err = -1;
+ if (mObserver != null) {
+ Log.d(TAG, "restorePackage() called during active restore");
+ return -1;
+ }
+ mObserver = new RestoreObserverWrapper(mContext, observer);
+ BackupManagerMonitorWrapper monitorWrapper = monitor == null
+ ? null
+ : new BackupManagerMonitorWrapper(monitor);
+ try {
+ err = mBinder.restorePackage(packageName, mObserver, monitorWrapper);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Can't contact server to restore package");
+ }
+ return err;
+ }
+
+
+ /**
+ * Restore a single application from backup. The data will be restored from the
+ * current backup dataset if the given package has stored data there, or from
+ * the dataset used during the last full device setup operation if the current
+ * backup dataset has no matching data. If no backup data exists for this package
+ * in either source, a nonzero value will be returned.
+ *
+ * @return Zero on success; nonzero on error. The observer will only receive
+ * progress callbacks if this method returned zero.
+ * @param packageName The name of the package whose data to restore. If this is
+ * not the name of the caller's own package, then the android.permission.BACKUP
+ * permission must be held.
+ * @param observer If non-null, this binder points to an object that will receive
+ * progress callbacks during the restore operation.
+ */
+ public int restorePackage(String packageName, RestoreObserver observer) {
+ return restorePackage(packageName, observer, null);
+ }
+
+ /**
+ * End this restore session. After this method is called, the RestoreSession
+ * object is no longer valid.
+ *
+ * <p><b>Note:</b> The caller <i>must</i> invoke this method to end the restore session,
+ * even if {@link #restorePackage(String, RestoreObserver)} failed.
+ */
+ public void endRestoreSession() {
+ try {
+ mBinder.endRestoreSession();
+ } catch (RemoteException e) {
+ Log.d(TAG, "Can't contact server to get available sets");
+ } finally {
+ mBinder = null;
+ }
+ }
+
+ /*
+ * Nonpublic implementation here
+ */
+
+ RestoreSession(Context context, IRestoreSession binder) {
+ mContext = context;
+ mBinder = binder;
+ }
+
+ /*
+ * We wrap incoming binder calls with a private class implementation that
+ * redirects them into main-thread actions. This serializes the restore
+ * progress callbacks nicely within the usual main-thread lifecycle pattern.
+ */
+ private class RestoreObserverWrapper extends IRestoreObserver.Stub {
+ final Handler mHandler;
+ final RestoreObserver mAppObserver;
+
+ static final int MSG_RESTORE_STARTING = 1;
+ static final int MSG_UPDATE = 2;
+ static final int MSG_RESTORE_FINISHED = 3;
+ static final int MSG_RESTORE_SETS_AVAILABLE = 4;
+
+ RestoreObserverWrapper(Context context, RestoreObserver appObserver) {
+ mHandler = new Handler(context.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_RESTORE_STARTING:
+ mAppObserver.restoreStarting(msg.arg1);
+ break;
+ case MSG_UPDATE:
+ mAppObserver.onUpdate(msg.arg1, (String)msg.obj);
+ break;
+ case MSG_RESTORE_FINISHED:
+ mAppObserver.restoreFinished(msg.arg1);
+ break;
+ case MSG_RESTORE_SETS_AVAILABLE:
+ mAppObserver.restoreSetsAvailable((RestoreSet[])msg.obj);
+ break;
+ }
+ }
+ };
+ mAppObserver = appObserver;
+ }
+
+ // Binder calls into this object just enqueue on the main-thread handler
+ public void restoreSetsAvailable(RestoreSet[] result) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_RESTORE_SETS_AVAILABLE, result));
+ }
+
+ public void restoreStarting(int numPackages) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_RESTORE_STARTING, numPackages, 0));
+ }
+
+ public void onUpdate(int nowBeingRestored, String currentPackage) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE, nowBeingRestored, 0, currentPackage));
+ }
+
+ public void restoreFinished(int error) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_RESTORE_FINISHED, error, 0));
+ }
+ }
+
+ private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
+ final BackupManagerMonitor mMonitor;
+
+ BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
+ mMonitor = monitor;
+ }
+
+ @Override
+ public void onEvent(final Bundle event) throws RemoteException {
+ mMonitor.onEvent(event);
+ }
+ }
+}
diff --git a/android/app/backup/RestoreSet.java b/android/app/backup/RestoreSet.java
new file mode 100644
index 00000000..4a6316c5
--- /dev/null
+++ b/android/app/backup/RestoreSet.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Descriptive information about a set of backed-up app data available for restore.
+ * Used by IRestoreSession clients.
+ *
+ * @hide
+ */
+@SystemApi
+public class RestoreSet implements Parcelable {
+ /**
+ * Name of this restore set. May be user generated, may simply be the name
+ * of the handset model, e.g. "T-Mobile G1".
+ */
+ public String name;
+
+ /**
+ * Identifier of the device whose data this is. This will be as unique as
+ * is practically possible; for example, it might be an IMEI.
+ */
+ public String device;
+
+ /**
+ * Token that identifies this backup set unambiguously to the backup/restore
+ * transport. This is guaranteed to be valid for the duration of a restore
+ * session, but is meaningless once the session has ended.
+ */
+ public long token;
+
+
+ public RestoreSet() {
+ // Leave everything zero / null
+ }
+
+ public RestoreSet(String _name, String _dev, long _token) {
+ name = _name;
+ device = _dev;
+ token = _token;
+ }
+
+ // Parcelable implementation
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(name);
+ out.writeString(device);
+ out.writeLong(token);
+ }
+
+ public static final Parcelable.Creator<RestoreSet> CREATOR
+ = new Parcelable.Creator<RestoreSet>() {
+ public RestoreSet createFromParcel(Parcel in) {
+ return new RestoreSet(in);
+ }
+
+ public RestoreSet[] newArray(int size) {
+ return new RestoreSet[size];
+ }
+ };
+
+ private RestoreSet(Parcel in) {
+ name = in.readString();
+ device = in.readString();
+ token = in.readLong();
+ }
+}
diff --git a/android/app/backup/SelectBackupTransportCallback.java b/android/app/backup/SelectBackupTransportCallback.java
new file mode 100644
index 00000000..0c8a0dcb
--- /dev/null
+++ b/android/app/backup/SelectBackupTransportCallback.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.backup;
+
+import android.annotation.SystemApi;
+
+/**
+ * Callback class for receiving success or failure callbacks on selecting a backup transport. These
+ * methods will all be called on your application's main thread.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SelectBackupTransportCallback {
+
+ /**
+ * Called when BackupManager has successfully bound to the requested transport.
+ *
+ * @param transportName Name of the selected transport. This is the String returned by
+ * {@link BackupTransport#name()}.
+ */
+ public void onSuccess(String transportName){}
+
+ /**
+ * Called when BackupManager fails to bind to the requested transport.
+ *
+ * @param reason Error code denoting reason for failure.
+ */
+ public void onFailure(int reason){}
+}
diff --git a/android/app/backup/SharedPreferencesBackupHelper.java b/android/app/backup/SharedPreferencesBackupHelper.java
new file mode 100644
index 00000000..939616b3
--- /dev/null
+++ b/android/app/backup/SharedPreferencesBackupHelper.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.app.QueuedWork;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * A helper class that can be used in conjunction with
+ * {@link android.app.backup.BackupAgentHelper} to manage the backup of
+ * {@link android.content.SharedPreferences}. Whenever a backup is performed, it
+ * will back up all named shared preferences that have changed since the last
+ * backup operation.
+ * <p>
+ * To use this class, the application's backup agent class should extend
+ * {@link android.app.backup.BackupAgentHelper}. Then, in the agent's
+ * {@link BackupAgent#onCreate()} method, an instance of this class should be
+ * allocated and installed as a backup/restore handler within the BackupAgentHelper
+ * framework. For example, an agent supporting backup and restore for
+ * an application with two groups of {@link android.content.SharedPreferences}
+ * data might look something like this:
+ * <pre>
+ * import android.app.backup.BackupAgentHelper;
+ * import android.app.backup.SharedPreferencesBackupHelper;
+ *
+ * public class MyBackupAgent extends BackupAgentHelper {
+ * // The names of the SharedPreferences groups that the application maintains. These
+ * // are the same strings that are passed to {@link Context#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 SharedPreferenceBackupHelper'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>
+ * No further implementation is needed; the {@link BackupAgentHelper} mechanism automatically
+ * dispatches the
+ * {@link BackupAgent#onBackup(android.os.ParcelFileDescriptor, BackupDataOutput, android.os.ParcelFileDescriptor) BackupAgent.onBackup()}
+ * and
+ * {@link BackupAgent#onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor) BackupAgent.onRestore()}
+ * callbacks to the SharedPreferencesBackupHelper as appropriate.
+ */
+public class SharedPreferencesBackupHelper extends FileBackupHelperBase implements BackupHelper {
+ private static final String TAG = "SharedPreferencesBackupHelper";
+ private static final boolean DEBUG = false;
+
+ private Context mContext;
+ private String[] mPrefGroups;
+
+ /**
+ * Construct a helper for backing up and restoring the
+ * {@link android.content.SharedPreferences} under the given names.
+ *
+ * @param context The application {@link android.content.Context}
+ * @param prefGroups The names of each {@link android.content.SharedPreferences} file to
+ * back up
+ */
+ public SharedPreferencesBackupHelper(Context context, String... prefGroups) {
+ super(context);
+
+ mContext = context;
+ mPrefGroups = prefGroups;
+ }
+
+ /**
+ * Backs up the configured {@link android.content.SharedPreferences} groups.
+ */
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) {
+ Context context = mContext;
+
+ // If a SharedPreference has an outstanding write in flight,
+ // wait for it to finish flushing to disk.
+ QueuedWork.waitToFinish();
+
+ // make filenames for the prefGroups
+ String[] prefGroups = mPrefGroups;
+ final int N = prefGroups.length;
+ String[] files = new String[N];
+ for (int i=0; i<N; i++) {
+ files[i] = context.getSharedPrefsFile(prefGroups[i]).getAbsolutePath();
+ }
+
+ // go
+ performBackup_checked(oldState, data, newState, files, prefGroups);
+ }
+
+ /**
+ * Restores one entity from the restore data stream to its proper shared
+ * preferences file store.
+ */
+ public void restoreEntity(BackupDataInputStream data) {
+ Context context = mContext;
+
+ String key = data.getKey();
+ if (DEBUG) Log.d(TAG, "got entity '" + key + "' size=" + data.size());
+
+ if (isKeyInList(key, mPrefGroups)) {
+ File f = context.getSharedPrefsFile(key).getAbsoluteFile();
+ writeFile(f, data);
+ }
+ }
+}
diff --git a/android/app/backup/WallpaperBackupHelper.java b/android/app/backup/WallpaperBackupHelper.java
new file mode 100644
index 00000000..36f5f967
--- /dev/null
+++ b/android/app/backup/WallpaperBackupHelper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * We no longer back up wallpapers with this helper, but we do need to process restores
+ * of legacy backup payloads. We just take the restored image as-is and apply it as the
+ * system wallpaper using the public "set the wallpaper" API.
+ *
+ * @hide
+ */
+public class WallpaperBackupHelper extends FileBackupHelperBase implements BackupHelper {
+ private static final String TAG = "WallpaperBackupHelper";
+ private static final boolean DEBUG = false;
+
+ // Key that legacy wallpaper imagery was stored under
+ public static final String WALLPAPER_IMAGE_KEY =
+ "/data/data/com.android.settings/files/wallpaper";
+ public static final String WALLPAPER_INFO_KEY = "/data/system/wallpaper_info.xml";
+
+ // Stage file that the restored imagery is stored to prior to being applied
+ // as the system wallpaper.
+ private static final String STAGE_FILE =
+ new File(Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM),
+ "wallpaper-tmp").getAbsolutePath();
+
+ private final String[] mKeys;
+ private final WallpaperManager mWpm;
+
+ /**
+ * Legacy wallpaper restores, from back when the imagery was stored under the
+ * "android" system package as file key/value entities.
+ *
+ * @param context
+ * @param files
+ */
+ public WallpaperBackupHelper(Context context, String[] keys) {
+ super(context);
+
+ mContext = context;
+ mKeys = keys;
+
+ mWpm = (WallpaperManager) context.getSystemService(Context.WALLPAPER_SERVICE);
+ }
+
+ /**
+ * Based on oldState, determine which of the files from the application's data directory
+ * need to be backed up, write them to the data stream, and fill in newState with the
+ * state as it exists now.
+ */
+ @Override
+ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) {
+ // Intentionally no-op; we don't back up the wallpaper this way any more.
+ }
+
+ /**
+ * Restore one absolute file entity from the restore stream. If we're restoring the
+ * magic wallpaper file, apply it as the system wallpaper.
+ */
+ @Override
+ public void restoreEntity(BackupDataInputStream data) {
+ final String key = data.getKey();
+ if (isKeyInList(key, mKeys)) {
+ if (key.equals(WALLPAPER_IMAGE_KEY)) {
+ // restore the file to the stage for inspection
+ File stage = new File(STAGE_FILE);
+ try {
+ if (writeFile(stage, data)) {
+ try (FileInputStream in = new FileInputStream(stage)) {
+ mWpm.setStream(in);
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to set restored wallpaper: " + e.getMessage());
+ }
+ } else {
+ Slog.e(TAG, "Unable to save restored wallpaper");
+ }
+ } finally {
+ stage.delete();
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java
new file mode 100644
index 00000000..87e516ca
--- /dev/null
+++ b/android/app/job/JobInfo.java
@@ -0,0 +1,1189 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.job;
+
+import static android.util.TimeUtils.formatDuration;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the
+ * parameters required to schedule work against the calling application. These are constructed
+ * using the {@link JobInfo.Builder}.
+ * You must specify at least one sort of constraint on the JobInfo object that you are creating.
+ * The goal here is to provide the scheduler with high-level semantics about the work you want to
+ * accomplish. Doing otherwise with throw an exception in your app.
+ */
+public class JobInfo implements Parcelable {
+ private static String TAG = "JobInfo";
+
+ /** @hide */
+ @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
+ NETWORK_TYPE_NONE,
+ NETWORK_TYPE_ANY,
+ NETWORK_TYPE_UNMETERED,
+ NETWORK_TYPE_NOT_ROAMING,
+ NETWORK_TYPE_METERED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NetworkType {}
+
+ /** Default. */
+ public static final int NETWORK_TYPE_NONE = 0;
+ /** This job requires network connectivity. */
+ public static final int NETWORK_TYPE_ANY = 1;
+ /** This job requires network connectivity that is unmetered. */
+ public static final int NETWORK_TYPE_UNMETERED = 2;
+ /** This job requires network connectivity that is not roaming. */
+ public static final int NETWORK_TYPE_NOT_ROAMING = 3;
+ /** This job requires metered connectivity such as most cellular data networks. */
+ public static final int NETWORK_TYPE_METERED = 4;
+
+ /**
+ * Amount of backoff a job has initially by default, in milliseconds.
+ */
+ public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 30 seconds.
+
+ /**
+ * Maximum backoff we allow for a job, in milliseconds.
+ */
+ public static final long MAX_BACKOFF_DELAY_MILLIS = 5 * 60 * 60 * 1000; // 5 hours.
+
+ /** @hide */
+ @IntDef(prefix = { "BACKOFF_POLICY_" }, value = {
+ BACKOFF_POLICY_LINEAR,
+ BACKOFF_POLICY_EXPONENTIAL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BackoffPolicy {}
+
+ /**
+ * Linearly back-off a failed job. See
+ * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}
+ * retry_time(current_time, num_failures) =
+ * current_time + initial_backoff_millis * num_failures, num_failures >= 1
+ */
+ public static final int BACKOFF_POLICY_LINEAR = 0;
+
+ /**
+ * Exponentially back-off a failed job. See
+ * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}
+ *
+ * retry_time(current_time, num_failures) =
+ * current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1
+ */
+ public static final int BACKOFF_POLICY_EXPONENTIAL = 1;
+
+ /* Minimum interval for a periodic job, in milliseconds. */
+ private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L; // 15 minutes
+
+ /* Minimum flex for a periodic job, in milliseconds. */
+ private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes
+
+ /**
+ * Minimum backoff interval for a job, in milliseconds
+ * @hide
+ */
+ public static final long MIN_BACKOFF_MILLIS = 10 * 1000L; // 10 seconds
+
+ /**
+ * Query the minimum interval allowed for periodic scheduled jobs. Attempting
+ * to declare a smaller period that this when scheduling a job will result in a
+ * job that is still periodic, but will run with this effective period.
+ *
+ * @return The minimum available interval for scheduling periodic jobs, in milliseconds.
+ */
+ public static final long getMinPeriodMillis() {
+ return MIN_PERIOD_MILLIS;
+ }
+
+ /**
+ * Query the minimum flex time allowed for periodic scheduled jobs. Attempting
+ * to declare a shorter flex time than this when scheduling such a job will
+ * result in this amount as the effective flex time for the job.
+ *
+ * @return The minimum available flex time for scheduling periodic jobs, in milliseconds.
+ */
+ public static final long getMinFlexMillis() {
+ return MIN_FLEX_MILLIS;
+ }
+
+ /**
+ * Query the minimum automatic-reschedule backoff interval permitted for jobs.
+ * @hide
+ */
+ public static final long getMinBackoffMillis() {
+ return MIN_BACKOFF_MILLIS;
+ }
+
+ /**
+ * Default type of backoff.
+ * @hide
+ */
+ public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL;
+
+ /**
+ * Default of {@link #getPriority}.
+ * @hide
+ */
+ public static final int PRIORITY_DEFAULT = 0;
+
+ /**
+ * Value of {@link #getPriority} for expedited syncs.
+ * @hide
+ */
+ public static final int PRIORITY_SYNC_EXPEDITED = 10;
+
+ /**
+ * Value of {@link #getPriority} for first time initialization syncs.
+ * @hide
+ */
+ public static final int PRIORITY_SYNC_INITIALIZATION = 20;
+
+ /**
+ * Value of {@link #getPriority} for a foreground app (overrides the supplied
+ * JobInfo priority if it is smaller).
+ * @hide
+ */
+ public static final int PRIORITY_FOREGROUND_APP = 30;
+
+ /**
+ * Value of {@link #getPriority} for the current top app (overrides the supplied
+ * JobInfo priority if it is smaller).
+ * @hide
+ */
+ public static final int PRIORITY_TOP_APP = 40;
+
+ /**
+ * Adjustment of {@link #getPriority} if the app has often (50% or more of the time)
+ * been running jobs.
+ * @hide
+ */
+ public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40;
+
+ /**
+ * Adjustment of {@link #getPriority} if the app has always (90% or more of the time)
+ * been running jobs.
+ * @hide
+ */
+ public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80;
+
+ /**
+ * Indicates that the implementation of this job will be using
+ * {@link JobService#startForeground(int, android.app.Notification)} to run
+ * in the foreground.
+ * <p>
+ * When set, the internal scheduling of this job will ignore any background
+ * network restrictions for the requesting app. Note that this flag alone
+ * doesn't actually place your {@link JobService} in the foreground; you
+ * still need to post the notification yourself.
+ * <p>
+ * To use this flag, the caller must hold the
+ * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} permission.
+ *
+ * @hide
+ */
+ public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0;
+
+ /**
+ * @hide
+ */
+ public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
+
+ /**
+ * @hide
+ */
+ public static final int CONSTRAINT_FLAG_BATTERY_NOT_LOW = 1 << 1;
+
+ /**
+ * @hide
+ */
+ public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2;
+
+ /**
+ * @hide
+ */
+ public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3;
+
+ private final int jobId;
+ private final PersistableBundle extras;
+ private final Bundle transientExtras;
+ private final ClipData clipData;
+ private final int clipGrantFlags;
+ private final ComponentName service;
+ private final int constraintFlags;
+ private final TriggerContentUri[] triggerContentUris;
+ private final long triggerContentUpdateDelay;
+ private final long triggerContentMaxDelay;
+ private final boolean hasEarlyConstraint;
+ private final boolean hasLateConstraint;
+ private final int networkType;
+ private final long minLatencyMillis;
+ private final long maxExecutionDelayMillis;
+ private final boolean isPeriodic;
+ private final boolean isPersisted;
+ private final long intervalMillis;
+ private final long flexMillis;
+ private final long initialBackoffMillis;
+ private final int backoffPolicy;
+ private final int priority;
+ private final int flags;
+
+ /**
+ * Unique job id associated with this application (uid). This is the same job ID
+ * you supplied in the {@link Builder} constructor.
+ */
+ public int getId() {
+ return jobId;
+ }
+
+ /**
+ * Bundle of extras which are returned to your application at execution time.
+ */
+ public @NonNull PersistableBundle getExtras() {
+ return extras;
+ }
+
+ /**
+ * Bundle of transient extras which are returned to your application at execution time,
+ * but not persisted by the system.
+ */
+ public @NonNull Bundle getTransientExtras() {
+ return transientExtras;
+ }
+
+ /**
+ * ClipData of information that is returned to your application at execution time,
+ * but not persisted by the system.
+ */
+ public @Nullable ClipData getClipData() {
+ return clipData;
+ }
+
+ /**
+ * Permission grants that go along with {@link #getClipData}.
+ */
+ public int getClipGrantFlags() {
+ return clipGrantFlags;
+ }
+
+ /**
+ * Name of the service endpoint that will be called back into by the JobScheduler.
+ */
+ public @NonNull ComponentName getService() {
+ return service;
+ }
+
+ /** @hide */
+ public int getPriority() {
+ return priority;
+ }
+
+ /** @hide */
+ public int getFlags() {
+ return flags;
+ }
+
+ /**
+ * Whether this job needs the device to be plugged in.
+ */
+ public boolean isRequireCharging() {
+ return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0;
+ }
+
+ /**
+ * Whether this job needs the device's battery level to not be at below the critical threshold.
+ */
+ public boolean isRequireBatteryNotLow() {
+ return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0;
+ }
+
+ /**
+ * Whether this job needs the device to be in an Idle maintenance window.
+ */
+ public boolean isRequireDeviceIdle() {
+ return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0;
+ }
+
+ /**
+ * Whether this job needs the device's storage to not be low.
+ */
+ public boolean isRequireStorageNotLow() {
+ return (constraintFlags & CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0;
+ }
+
+ /**
+ * @hide
+ */
+ public int getConstraintFlags() {
+ return constraintFlags;
+ }
+
+ /**
+ * Which content: URIs must change for the job to be scheduled. Returns null
+ * if there are none required.
+ */
+ public @Nullable TriggerContentUri[] getTriggerContentUris() {
+ return triggerContentUris;
+ }
+
+ /**
+ * When triggering on content URI changes, this is the delay from when a change
+ * is detected until the job is scheduled.
+ */
+ public long getTriggerContentUpdateDelay() {
+ return triggerContentUpdateDelay;
+ }
+
+ /**
+ * When triggering on content URI changes, this is the maximum delay we will
+ * use before scheduling the job.
+ */
+ public long getTriggerContentMaxDelay() {
+ return triggerContentMaxDelay;
+ }
+
+ /**
+ * The kind of connectivity requirements that the job has.
+ */
+ public @NetworkType int getNetworkType() {
+ return networkType;
+ }
+
+ /**
+ * Set for a job that does not recur periodically, to specify a delay after which the job
+ * will be eligible for execution. This value is not set if the job recurs periodically.
+ */
+ public long getMinLatencyMillis() {
+ return minLatencyMillis;
+ }
+
+ /**
+ * See {@link Builder#setOverrideDeadline(long)}. This value is not set if the job recurs
+ * periodically.
+ */
+ public long getMaxExecutionDelayMillis() {
+ return maxExecutionDelayMillis;
+ }
+
+ /**
+ * Track whether this job will repeat with a given period.
+ */
+ public boolean isPeriodic() {
+ return isPeriodic;
+ }
+
+ /**
+ * @return Whether or not this job should be persisted across device reboots.
+ */
+ public boolean isPersisted() {
+ return isPersisted;
+ }
+
+ /**
+ * Set to the interval between occurrences of this job. This value is <b>not</b> set if the
+ * job does not recur periodically.
+ */
+ public long getIntervalMillis() {
+ final long minInterval = getMinPeriodMillis();
+ return intervalMillis >= minInterval ? intervalMillis : minInterval;
+ }
+
+ /**
+ * Flex time for this job. Only valid if this is a periodic job. The job can
+ * execute at any time in a window of flex length at the end of the period.
+ */
+ public long getFlexMillis() {
+ long interval = getIntervalMillis();
+ long percentClamp = 5 * interval / 100;
+ long clampedFlex = Math.max(flexMillis, Math.max(percentClamp, getMinFlexMillis()));
+ return clampedFlex <= interval ? clampedFlex : interval;
+ }
+
+ /**
+ * The amount of time the JobScheduler will wait before rescheduling a failed job. This value
+ * will be increased depending on the backoff policy specified at job creation time. Defaults
+ * to 30 seconds, minimum is currently 10 seconds.
+ */
+ public long getInitialBackoffMillis() {
+ final long minBackoff = getMinBackoffMillis();
+ return initialBackoffMillis >= minBackoff ? initialBackoffMillis : minBackoff;
+ }
+
+ /**
+ * Return the backoff policy of this job.
+ */
+ public @BackoffPolicy int getBackoffPolicy() {
+ return backoffPolicy;
+ }
+
+ /**
+ * User can specify an early constraint of 0L, which is valid, so we keep track of whether the
+ * function was called at all.
+ * @hide
+ */
+ public boolean hasEarlyConstraint() {
+ return hasEarlyConstraint;
+ }
+
+ /**
+ * User can specify a late constraint of 0L, which is valid, so we keep track of whether the
+ * function was called at all.
+ * @hide
+ */
+ public boolean hasLateConstraint() {
+ return hasLateConstraint;
+ }
+
+ private static boolean kindofEqualsBundle(BaseBundle a, BaseBundle b) {
+ return (a == b) || (a != null && a.kindofEquals(b));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JobInfo)) {
+ return false;
+ }
+ JobInfo j = (JobInfo) o;
+ if (jobId != j.jobId) {
+ return false;
+ }
+ // XXX won't be correct if one is parcelled and the other not.
+ if (!kindofEqualsBundle(extras, j.extras)) {
+ return false;
+ }
+ // XXX won't be correct if one is parcelled and the other not.
+ if (!kindofEqualsBundle(transientExtras, j.transientExtras)) {
+ return false;
+ }
+ // XXX for now we consider two different clip data objects to be different,
+ // regardless of whether their contents are the same.
+ if (clipData != j.clipData) {
+ return false;
+ }
+ if (clipGrantFlags != j.clipGrantFlags) {
+ return false;
+ }
+ if (!Objects.equals(service, j.service)) {
+ return false;
+ }
+ if (constraintFlags != j.constraintFlags) {
+ return false;
+ }
+ if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) {
+ return false;
+ }
+ if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) {
+ return false;
+ }
+ if (triggerContentMaxDelay != j.triggerContentMaxDelay) {
+ return false;
+ }
+ if (hasEarlyConstraint != j.hasEarlyConstraint) {
+ return false;
+ }
+ if (hasLateConstraint != j.hasLateConstraint) {
+ return false;
+ }
+ if (networkType != j.networkType) {
+ return false;
+ }
+ if (minLatencyMillis != j.minLatencyMillis) {
+ return false;
+ }
+ if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) {
+ return false;
+ }
+ if (isPeriodic != j.isPeriodic) {
+ return false;
+ }
+ if (isPersisted != j.isPersisted) {
+ return false;
+ }
+ if (intervalMillis != j.intervalMillis) {
+ return false;
+ }
+ if (flexMillis != j.flexMillis) {
+ return false;
+ }
+ if (initialBackoffMillis != j.initialBackoffMillis) {
+ return false;
+ }
+ if (backoffPolicy != j.backoffPolicy) {
+ return false;
+ }
+ if (priority != j.priority) {
+ return false;
+ }
+ if (flags != j.flags) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = jobId;
+ if (extras != null) {
+ hashCode = 31 * hashCode + extras.hashCode();
+ }
+ if (transientExtras != null) {
+ hashCode = 31 * hashCode + transientExtras.hashCode();
+ }
+ if (clipData != null) {
+ hashCode = 31 * hashCode + clipData.hashCode();
+ }
+ hashCode = 31*hashCode + clipGrantFlags;
+ if (service != null) {
+ hashCode = 31 * hashCode + service.hashCode();
+ }
+ hashCode = 31 * hashCode + constraintFlags;
+ if (triggerContentUris != null) {
+ hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris);
+ }
+ hashCode = 31 * hashCode + Long.hashCode(triggerContentUpdateDelay);
+ hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay);
+ hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint);
+ hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint);
+ hashCode = 31 * hashCode + networkType;
+ hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis);
+ hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis);
+ hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic);
+ hashCode = 31 * hashCode + Boolean.hashCode(isPersisted);
+ hashCode = 31 * hashCode + Long.hashCode(intervalMillis);
+ hashCode = 31 * hashCode + Long.hashCode(flexMillis);
+ hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis);
+ hashCode = 31 * hashCode + backoffPolicy;
+ hashCode = 31 * hashCode + priority;
+ hashCode = 31 * hashCode + flags;
+ return hashCode;
+ }
+
+ private JobInfo(Parcel in) {
+ jobId = in.readInt();
+ extras = in.readPersistableBundle();
+ transientExtras = in.readBundle();
+ if (in.readInt() != 0) {
+ clipData = ClipData.CREATOR.createFromParcel(in);
+ clipGrantFlags = in.readInt();
+ } else {
+ clipData = null;
+ clipGrantFlags = 0;
+ }
+ service = in.readParcelable(null);
+ constraintFlags = in.readInt();
+ triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
+ triggerContentUpdateDelay = in.readLong();
+ triggerContentMaxDelay = in.readLong();
+ networkType = in.readInt();
+ minLatencyMillis = in.readLong();
+ maxExecutionDelayMillis = in.readLong();
+ isPeriodic = in.readInt() == 1;
+ isPersisted = in.readInt() == 1;
+ intervalMillis = in.readLong();
+ flexMillis = in.readLong();
+ initialBackoffMillis = in.readLong();
+ backoffPolicy = in.readInt();
+ hasEarlyConstraint = in.readInt() == 1;
+ hasLateConstraint = in.readInt() == 1;
+ priority = in.readInt();
+ flags = in.readInt();
+ }
+
+ private JobInfo(JobInfo.Builder b) {
+ jobId = b.mJobId;
+ extras = b.mExtras.deepCopy();
+ transientExtras = b.mTransientExtras.deepCopy();
+ clipData = b.mClipData;
+ clipGrantFlags = b.mClipGrantFlags;
+ service = b.mJobService;
+ constraintFlags = b.mConstraintFlags;
+ triggerContentUris = b.mTriggerContentUris != null
+ ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
+ : null;
+ triggerContentUpdateDelay = b.mTriggerContentUpdateDelay;
+ triggerContentMaxDelay = b.mTriggerContentMaxDelay;
+ networkType = b.mNetworkType;
+ minLatencyMillis = b.mMinLatencyMillis;
+ maxExecutionDelayMillis = b.mMaxExecutionDelayMillis;
+ isPeriodic = b.mIsPeriodic;
+ isPersisted = b.mIsPersisted;
+ intervalMillis = b.mIntervalMillis;
+ flexMillis = b.mFlexMillis;
+ initialBackoffMillis = b.mInitialBackoffMillis;
+ backoffPolicy = b.mBackoffPolicy;
+ hasEarlyConstraint = b.mHasEarlyConstraint;
+ hasLateConstraint = b.mHasLateConstraint;
+ priority = b.mPriority;
+ flags = b.mFlags;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(jobId);
+ out.writePersistableBundle(extras);
+ out.writeBundle(transientExtras);
+ if (clipData != null) {
+ out.writeInt(1);
+ clipData.writeToParcel(out, flags);
+ out.writeInt(clipGrantFlags);
+ } else {
+ out.writeInt(0);
+ }
+ out.writeParcelable(service, flags);
+ out.writeInt(constraintFlags);
+ out.writeTypedArray(triggerContentUris, flags);
+ out.writeLong(triggerContentUpdateDelay);
+ out.writeLong(triggerContentMaxDelay);
+ out.writeInt(networkType);
+ out.writeLong(minLatencyMillis);
+ out.writeLong(maxExecutionDelayMillis);
+ out.writeInt(isPeriodic ? 1 : 0);
+ out.writeInt(isPersisted ? 1 : 0);
+ out.writeLong(intervalMillis);
+ out.writeLong(flexMillis);
+ out.writeLong(initialBackoffMillis);
+ out.writeInt(backoffPolicy);
+ out.writeInt(hasEarlyConstraint ? 1 : 0);
+ out.writeInt(hasLateConstraint ? 1 : 0);
+ out.writeInt(priority);
+ out.writeInt(this.flags);
+ }
+
+ public static final Creator<JobInfo> CREATOR = new Creator<JobInfo>() {
+ @Override
+ public JobInfo createFromParcel(Parcel in) {
+ return new JobInfo(in);
+ }
+
+ @Override
+ public JobInfo[] newArray(int size) {
+ return new JobInfo[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "(job:" + jobId + "/" + service.flattenToShortString() + ")";
+ }
+
+ /**
+ * Information about a content URI modification that a job would like to
+ * trigger on.
+ */
+ public static final class TriggerContentUri implements Parcelable {
+ private final Uri mUri;
+ private final int mFlags;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_NOTIFY_FOR_DESCENDANTS,
+ })
+ public @interface Flags { }
+
+ /**
+ * Flag for trigger: also trigger if any descendants of the given URI change.
+ * Corresponds to the <var>notifyForDescendants</var> of
+ * {@link android.content.ContentResolver#registerContentObserver}.
+ */
+ public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1<<0;
+
+ /**
+ * Create a new trigger description.
+ * @param uri The URI to observe. Must be non-null.
+ * @param flags Flags for the observer.
+ */
+ public TriggerContentUri(@NonNull Uri uri, @Flags int flags) {
+ mUri = uri;
+ mFlags = flags;
+ }
+
+ /**
+ * Return the Uri this trigger was created for.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Return the flags supplied for the trigger.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof TriggerContentUri)) {
+ return false;
+ }
+ TriggerContentUri t = (TriggerContentUri) o;
+ return Objects.equals(t.mUri, mUri) && t.mFlags == mFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mUri == null ? 0 : mUri.hashCode()) ^ mFlags;
+ }
+
+ private TriggerContentUri(Parcel in) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ mFlags = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mUri.writeToParcel(out, flags);
+ out.writeInt(mFlags);
+ }
+
+ public static final Creator<TriggerContentUri> CREATOR = new Creator<TriggerContentUri>() {
+ @Override
+ public TriggerContentUri createFromParcel(Parcel in) {
+ return new TriggerContentUri(in);
+ }
+
+ @Override
+ public TriggerContentUri[] newArray(int size) {
+ return new TriggerContentUri[size];
+ }
+ };
+ }
+
+ /** Builder class for constructing {@link JobInfo} objects. */
+ public static final class Builder {
+ private final int mJobId;
+ private final ComponentName mJobService;
+ private PersistableBundle mExtras = PersistableBundle.EMPTY;
+ private Bundle mTransientExtras = Bundle.EMPTY;
+ private ClipData mClipData;
+ private int mClipGrantFlags;
+ private int mPriority = PRIORITY_DEFAULT;
+ private int mFlags;
+ // Requirements.
+ private int mConstraintFlags;
+ private int mNetworkType;
+ private ArrayList<TriggerContentUri> mTriggerContentUris;
+ private long mTriggerContentUpdateDelay = -1;
+ private long mTriggerContentMaxDelay = -1;
+ private boolean mIsPersisted;
+ // One-off parameters.
+ private long mMinLatencyMillis;
+ private long mMaxExecutionDelayMillis;
+ // Periodic parameters.
+ private boolean mIsPeriodic;
+ private boolean mHasEarlyConstraint;
+ private boolean mHasLateConstraint;
+ private long mIntervalMillis;
+ private long mFlexMillis;
+ // Back-off parameters.
+ private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS;
+ private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY;
+ /** Easy way to track whether the client has tried to set a back-off policy. */
+ private boolean mBackoffPolicySet = false;
+
+ /**
+ * Initialize a new Builder to construct a {@link JobInfo}.
+ *
+ * @param jobId Application-provided id for this job. Subsequent calls to cancel, or
+ * jobs created with the same jobId, will update the pre-existing job with
+ * the same id. This ID must be unique across all clients of the same uid
+ * (not just the same package). You will want to make sure this is a stable
+ * id across app updates, so probably not based on a resource ID.
+ * @param jobService The endpoint that you implement that will receive the callback from the
+ * JobScheduler.
+ */
+ public Builder(int jobId, @NonNull ComponentName jobService) {
+ mJobService = jobService;
+ mJobId = jobId;
+ }
+
+ /** @hide */
+ public Builder setPriority(int priority) {
+ mPriority = priority;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setFlags(int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Set optional extras. This is persisted, so we only allow primitive types.
+ * @param extras Bundle containing extras you want the scheduler to hold on to for you.
+ */
+ public Builder setExtras(@NonNull PersistableBundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Set optional transient extras.
+ *
+ * <p>Because setting this property is not compatible with persisted
+ * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
+ *
+ * @param extras Bundle containing extras you want the scheduler to hold on to for you.
+ */
+ public Builder setTransientExtras(@NonNull Bundle extras) {
+ mTransientExtras = extras;
+ return this;
+ }
+
+ /**
+ * Set a {@link ClipData} associated with this Job.
+ *
+ * <p>The main purpose of providing a ClipData is to allow granting of
+ * URI permissions for data associated with the clip. The exact kind
+ * of permission grant to perform is specified through <var>grantFlags</var>.
+ *
+ * <p>If the ClipData contains items that are Intents, any
+ * grant flags in those Intents will be ignored. Only flags provided as an argument
+ * to this method are respected, and will be applied to all Uri or
+ * Intent items in the clip (or sub-items of the clip).
+ *
+ * <p>Because setting this property is not compatible with persisted
+ * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
+ *
+ * @param clip The new clip to set. May be null to clear the current clip.
+ * @param grantFlags The desired permissions to grant for any URIs. This should be
+ * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION},
+ * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and
+ * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
+ */
+ public Builder setClipData(@Nullable ClipData clip, int grantFlags) {
+ mClipData = clip;
+ mClipGrantFlags = grantFlags;
+ return this;
+ }
+
+ /**
+ * Set some description of the kind of network type your job needs to have.
+ * Not calling this function means the network is not necessary, as the default is
+ * {@link #NETWORK_TYPE_NONE}.
+ * Bear in mind that calling this function defines network as a strict requirement for your
+ * job. If the network requested is not available your job will never run. See
+ * {@link #setOverrideDeadline(long)} to change this behaviour.
+ */
+ public Builder setRequiredNetworkType(@NetworkType int networkType) {
+ mNetworkType = networkType;
+ return this;
+ }
+
+ /**
+ * Specify that to run this job, the device needs to be plugged in. This defaults to
+ * false.
+ * @param requiresCharging Whether or not the device is plugged in.
+ */
+ public Builder setRequiresCharging(boolean requiresCharging) {
+ mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING)
+ | (requiresCharging ? CONSTRAINT_FLAG_CHARGING : 0);
+ return this;
+ }
+
+ /**
+ * Specify that to run this job, the device's battery level must not be low.
+ * This defaults to false. If true, the job will only run when the battery level
+ * is not low, which is generally the point where the user is given a "low battery"
+ * warning.
+ * @param batteryNotLow Whether or not the device's battery level must not be low.
+ */
+ public Builder setRequiresBatteryNotLow(boolean batteryNotLow) {
+ mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_BATTERY_NOT_LOW)
+ | (batteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0);
+ return this;
+ }
+
+ /**
+ * Specify that to run, the job needs the device to be in idle mode. This defaults to
+ * false.
+ * <p>Idle mode is a loose definition provided by the system, which means that the device
+ * is not in use, and has not been in use for some time. As such, it is a good time to
+ * perform resource heavy jobs. Bear in mind that battery usage will still be attributed
+ * to your application, and surfaced to the user in battery stats.</p>
+ * @param requiresDeviceIdle Whether or not the device need be within an idle maintenance
+ * window.
+ */
+ public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
+ mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE)
+ | (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0);
+ return this;
+ }
+
+ /**
+ * Specify that to run this job, the device's available storage must not be low.
+ * This defaults to false. If true, the job will only run when the device is not
+ * in a low storage state, which is generally the point where the user is given a
+ * "low storage" warning.
+ * @param storageNotLow Whether or not the device's available storage must not be low.
+ */
+ public Builder setRequiresStorageNotLow(boolean storageNotLow) {
+ mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_STORAGE_NOT_LOW)
+ | (storageNotLow ? CONSTRAINT_FLAG_STORAGE_NOT_LOW : 0);
+ return this;
+ }
+
+ /**
+ * Add a new content: URI that will be monitored with a
+ * {@link android.database.ContentObserver}, and will cause the job to execute if changed.
+ * If you have any trigger content URIs associated with a job, it will not execute until
+ * there has been a change report for one or more of them.
+ *
+ * <p>Note that trigger URIs can not be used in combination with
+ * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}. To continually monitor
+ * for content changes, you need to schedule a new JobInfo observing the same URIs
+ * before you finish execution of the JobService handling the most recent changes.
+ * Following this pattern will ensure you do not lost any content changes: while your
+ * job is running, the system will continue monitoring for content changes, and propagate
+ * any it sees over to the next job you schedule.</p>
+ *
+ * <p>Because setting this property is not compatible with periodic or
+ * persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
+ *
+ * <p>The following example shows how this feature can be used to monitor for changes
+ * in the photos on a device.</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/PhotosContentJob.java
+ * job}
+ *
+ * @param uri The content: URI to monitor.
+ */
+ public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) {
+ if (mTriggerContentUris == null) {
+ mTriggerContentUris = new ArrayList<>();
+ }
+ mTriggerContentUris.add(uri);
+ return this;
+ }
+
+ /**
+ * Set the delay (in milliseconds) from when a content change is detected until
+ * the job is scheduled. If there are more changes during that time, the delay
+ * will be reset to start at the time of the most recent change.
+ * @param durationMs Delay after most recent content change, in milliseconds.
+ */
+ public Builder setTriggerContentUpdateDelay(long durationMs) {
+ mTriggerContentUpdateDelay = durationMs;
+ return this;
+ }
+
+ /**
+ * Set the maximum total delay (in milliseconds) that is allowed from the first
+ * time a content change is detected until the job is scheduled.
+ * @param durationMs Delay after initial content change, in milliseconds.
+ */
+ public Builder setTriggerContentMaxDelay(long durationMs) {
+ mTriggerContentMaxDelay = durationMs;
+ return this;
+ }
+
+ /**
+ * Specify that this job should recur with the provided interval, not more than once per
+ * period. You have no control over when within this interval this job will be executed,
+ * only the guarantee that it will be executed at most once within this interval.
+ * Setting this function on the builder with {@link #setMinimumLatency(long)} or
+ * {@link #setOverrideDeadline(long)} will result in an error.
+ * @param intervalMillis Millisecond interval for which this job will repeat.
+ */
+ public Builder setPeriodic(long intervalMillis) {
+ return setPeriodic(intervalMillis, intervalMillis);
+ }
+
+ /**
+ * Specify that this job should recur with the provided interval and flex. The job can
+ * execute at any time in a window of flex length at the end of the period.
+ * @param intervalMillis Millisecond interval for which this job will repeat. A minimum
+ * value of {@link #getMinPeriodMillis()} is enforced.
+ * @param flexMillis Millisecond flex for this job. Flex is clamped to be at least
+ * {@link #getMinFlexMillis()} or 5 percent of the period, whichever is
+ * higher.
+ */
+ public Builder setPeriodic(long intervalMillis, long flexMillis) {
+ mIsPeriodic = true;
+ mIntervalMillis = intervalMillis;
+ mFlexMillis = flexMillis;
+ mHasEarlyConstraint = mHasLateConstraint = true;
+ return this;
+ }
+
+ /**
+ * Specify that this job should be delayed by the provided amount of time.
+ * Because it doesn't make sense setting this property on a periodic job, doing so will
+ * throw an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.
+ * @param minLatencyMillis Milliseconds before which this job will not be considered for
+ * execution.
+ */
+ public Builder setMinimumLatency(long minLatencyMillis) {
+ mMinLatencyMillis = minLatencyMillis;
+ mHasEarlyConstraint = true;
+ return this;
+ }
+
+ /**
+ * Set deadline which is the maximum scheduling latency. The job will be run by this
+ * deadline even if other requirements are not met. Because it doesn't make sense setting
+ * this property on a periodic job, doing so will throw an
+ * {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.
+ */
+ public Builder setOverrideDeadline(long maxExecutionDelayMillis) {
+ mMaxExecutionDelayMillis = maxExecutionDelayMillis;
+ mHasLateConstraint = true;
+ return this;
+ }
+
+ /**
+ * Set up the back-off/retry policy.
+ * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at
+ * 5hrs.
+ * Note that trying to set a backoff criteria for a job with
+ * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build().
+ * This is because back-off typically does not make sense for these types of jobs. See
+ * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
+ * for more description of the return value for the case of a job executing while in idle
+ * mode.
+ * @param initialBackoffMillis Millisecond time interval to wait initially when job has
+ * failed.
+ */
+ public Builder setBackoffCriteria(long initialBackoffMillis,
+ @BackoffPolicy int backoffPolicy) {
+ mBackoffPolicySet = true;
+ mInitialBackoffMillis = initialBackoffMillis;
+ mBackoffPolicy = backoffPolicy;
+ return this;
+ }
+
+ /**
+ * Set whether or not to persist this job across device reboots.
+ *
+ * @param isPersisted True to indicate that the job will be written to
+ * disk and loaded at boot.
+ */
+ @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)
+ public Builder setPersisted(boolean isPersisted) {
+ mIsPersisted = isPersisted;
+ return this;
+ }
+
+ /**
+ * @return The job object to hand to the JobScheduler. This object is immutable.
+ */
+ public JobInfo build() {
+ // Allow jobs with no constraints - What am I, a database?
+ if (!mHasEarlyConstraint && !mHasLateConstraint && mConstraintFlags == 0 &&
+ mNetworkType == NETWORK_TYPE_NONE &&
+ mTriggerContentUris == null) {
+ throw new IllegalArgumentException("You're trying to build a job with no " +
+ "constraints, this is not allowed.");
+ }
+ // Check that a deadline was not set on a periodic job.
+ if (mIsPeriodic) {
+ if (mMaxExecutionDelayMillis != 0L) {
+ throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
+ "periodic job.");
+ }
+ if (mMinLatencyMillis != 0L) {
+ throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
+ "periodic job");
+ }
+ if (mTriggerContentUris != null) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "periodic job");
+ }
+ }
+ if (mIsPersisted) {
+ if (mTriggerContentUris != null) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "persisted job");
+ }
+ if (!mTransientExtras.isEmpty()) {
+ throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
+ "persisted job");
+ }
+ if (mClipData != null) {
+ throw new IllegalArgumentException("Can't call setClipData() on a " +
+ "persisted job");
+ }
+ }
+ if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
+ throw new IllegalArgumentException("An idle mode job will not respect any" +
+ " back-off policy, so calling setBackoffCriteria with" +
+ " setRequiresDeviceIdle is an error.");
+ }
+ JobInfo job = new JobInfo(this);
+ if (job.isPeriodic()) {
+ if (job.intervalMillis != job.getIntervalMillis()) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Specified interval for ")
+ .append(String.valueOf(mJobId))
+ .append(" is ");
+ formatDuration(mIntervalMillis, builder);
+ builder.append(". Clamped to ");
+ formatDuration(job.getIntervalMillis(), builder);
+ Log.w(TAG, builder.toString());
+ }
+ if (job.flexMillis != job.getFlexMillis()) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Specified flex for ")
+ .append(String.valueOf(mJobId))
+ .append(" is ");
+ formatDuration(mFlexMillis, builder);
+ builder.append(". Clamped to ");
+ formatDuration(job.getFlexMillis(), builder);
+ Log.w(TAG, builder.toString());
+ }
+ }
+ return job;
+ }
+ }
+
+}
diff --git a/android/app/job/JobParameters.java b/android/app/job/JobParameters.java
new file mode 100644
index 00000000..a6f6be22
--- /dev/null
+++ b/android/app/job/JobParameters.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.job;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.job.IJobCallback;
+import android.content.ClipData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+/**
+ * Contains the parameters used to configure/identify your job. You do not create this object
+ * yourself, instead it is handed in to your application by the System.
+ */
+public class JobParameters implements Parcelable {
+
+ /** @hide */
+ public static final int REASON_CANCELED = 0;
+ /** @hide */
+ public static final int REASON_CONSTRAINTS_NOT_SATISFIED = 1;
+ /** @hide */
+ public static final int REASON_PREEMPT = 2;
+ /** @hide */
+ public static final int REASON_TIMEOUT = 3;
+ /** @hide */
+ public static final int REASON_DEVICE_IDLE = 4;
+
+ /** @hide */
+ public static String getReasonName(int reason) {
+ switch (reason) {
+ case REASON_CANCELED: return "canceled";
+ case REASON_CONSTRAINTS_NOT_SATISFIED: return "constraints";
+ case REASON_PREEMPT: return "preempt";
+ case REASON_TIMEOUT: return "timeout";
+ case REASON_DEVICE_IDLE: return "device_idle";
+ default: return "unknown:" + reason;
+ }
+ }
+
+ private final int jobId;
+ private final PersistableBundle extras;
+ private final Bundle transientExtras;
+ private final ClipData clipData;
+ private final int clipGrantFlags;
+ private final IBinder callback;
+ private final boolean overrideDeadlineExpired;
+ private final Uri[] mTriggeredContentUris;
+ private final String[] mTriggeredContentAuthorities;
+
+ private int stopReason; // Default value of stopReason is REASON_CANCELED
+
+ /** @hide */
+ public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
+ Bundle transientExtras, ClipData clipData, int clipGrantFlags,
+ boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
+ String[] triggeredContentAuthorities) {
+ this.jobId = jobId;
+ this.extras = extras;
+ this.transientExtras = transientExtras;
+ this.clipData = clipData;
+ this.clipGrantFlags = clipGrantFlags;
+ this.callback = callback;
+ this.overrideDeadlineExpired = overrideDeadlineExpired;
+ this.mTriggeredContentUris = triggeredContentUris;
+ this.mTriggeredContentAuthorities = triggeredContentAuthorities;
+ }
+
+ /**
+ * @return The unique id of this job, specified at creation time.
+ */
+ public int getJobId() {
+ return jobId;
+ }
+
+ /**
+ * Reason onStopJob() was called on this job.
+ * @hide
+ */
+ public int getStopReason() {
+ return stopReason;
+ }
+
+ /**
+ * @return The extras you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will
+ * never be null. If you did not set any extras this will be an empty bundle.
+ */
+ public @NonNull PersistableBundle getExtras() {
+ return extras;
+ }
+
+ /**
+ * @return The transient extras you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setTransientExtras(android.os.Bundle)}. This will
+ * never be null. If you did not set any extras this will be an empty bundle.
+ */
+ public @NonNull Bundle getTransientExtras() {
+ return transientExtras;
+ }
+
+ /**
+ * @return The clip you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null
+ * if it was not set.
+ */
+ public @Nullable ClipData getClipData() {
+ return clipData;
+ }
+
+ /**
+ * @return The clip grant flags you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be 0
+ * if it was not set.
+ */
+ public int getClipGrantFlags() {
+ return clipGrantFlags;
+ }
+
+ /**
+ * For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
+ * provides an easy way to tell whether the job is being executed due to the deadline
+ * expiring. Note: If the job is running because its deadline expired, it implies that its
+ * constraints will not be met.
+ */
+ public boolean isOverrideDeadlineExpired() {
+ return overrideDeadlineExpired;
+ }
+
+ /**
+ * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this
+ * reports which URIs have triggered the job. This will be null if either no URIs have
+ * triggered it (it went off due to a deadline or other reason), or the number of changed
+ * URIs is too large to report. Whether or not the number of URIs is too large, you can
+ * always use {@link #getTriggeredContentAuthorities()} to determine whether the job was
+ * triggered due to any content changes and the authorities they are associated with.
+ */
+ public @Nullable Uri[] getTriggeredContentUris() {
+ return mTriggeredContentUris;
+ }
+
+ /**
+ * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this
+ * reports which content authorities have triggered the job. It will only be null if no
+ * authorities have triggered it -- that is, the job executed for some other reason, such
+ * as a deadline expiring. If this is non-null, you can use {@link #getTriggeredContentUris()}
+ * to retrieve the details of which URIs changed (as long as that has not exceeded the maximum
+ * number it can reported).
+ */
+ public @Nullable String[] getTriggeredContentAuthorities() {
+ return mTriggeredContentAuthorities;
+ }
+
+ /**
+ * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their
+ * currently running job. Calling this method when there is no more work available and all
+ * previously dequeued work has been completed will result in the system taking care of
+ * stopping the job for you --
+ * you should not call {@link JobService#jobFinished(JobParameters, boolean)} yourself
+ * (otherwise you risk losing an upcoming JobWorkItem that is being enqueued at the same time).
+ *
+ * <p>Once you are done with the {@link JobWorkItem} returned by this method, you must call
+ * {@link #completeWork(JobWorkItem)} with it to inform the system that you are done
+ * executing the work. The job will not be finished until all dequeued work has been
+ * completed. You do not, however, have to complete each returned work item before deqeueing
+ * the next one -- you can use {@link #dequeueWork()} multiple times before completing
+ * previous work if you want to process work in parallel, and you can complete the work
+ * in whatever order you want.</p>
+ *
+ * <p>If the job runs to the end of its available time period before all work has been
+ * completed, it will stop as normal. You should return true from
+ * {@link JobService#onStopJob(JobParameters)} in order to have the job rescheduled, and by
+ * doing so any pending as well as remaining uncompleted work will be re-queued
+ * for the next time the job runs.</p>
+ *
+ * <p>This example shows how to construct a JobService that will serially dequeue and
+ * process work that is available for it:</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/JobWorkService.java
+ * service}
+ *
+ * @return Returns a new {@link JobWorkItem} if there is one pending, otherwise null.
+ * If null is returned, the system will also stop the job if all work has also been completed.
+ * (This means that for correct operation, you must always call dequeueWork() after you have
+ * completed other work, to check either for more work or allow the system to stop the job.)
+ */
+ public @Nullable JobWorkItem dequeueWork() {
+ try {
+ return getCallback().dequeueWork(getJobId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Report the completion of executing a {@link JobWorkItem} previously returned by
+ * {@link #dequeueWork()}. This tells the system you are done with the
+ * work associated with that item, so it will not be returned again. Note that if this
+ * is the last work in the queue, completing it here will <em>not</em> finish the overall
+ * job -- for that to happen, you still need to call {@link #dequeueWork()}
+ * again.
+ *
+ * <p>If you are enqueueing work into a job, you must call this method for each piece
+ * of work you process. Do <em>not</em> call
+ * {@link JobService#jobFinished(JobParameters, boolean)}
+ * or else you can lose work in your queue.</p>
+ *
+ * @param work The work you have completed processing, as previously returned by
+ * {@link #dequeueWork()}
+ */
+ public void completeWork(@NonNull JobWorkItem work) {
+ try {
+ if (!getCallback().completeWork(getJobId(), work.getWorkId())) {
+ throw new IllegalArgumentException("Given work is not active: " + work);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public IJobCallback getCallback() {
+ return IJobCallback.Stub.asInterface(callback);
+ }
+
+ private JobParameters(Parcel in) {
+ jobId = in.readInt();
+ extras = in.readPersistableBundle();
+ transientExtras = in.readBundle();
+ if (in.readInt() != 0) {
+ clipData = ClipData.CREATOR.createFromParcel(in);
+ clipGrantFlags = in.readInt();
+ } else {
+ clipData = null;
+ clipGrantFlags = 0;
+ }
+ callback = in.readStrongBinder();
+ overrideDeadlineExpired = in.readInt() == 1;
+ mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
+ mTriggeredContentAuthorities = in.createStringArray();
+ stopReason = in.readInt();
+ }
+
+ /** @hide */
+ public void setStopReason(int reason) {
+ stopReason = reason;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(jobId);
+ dest.writePersistableBundle(extras);
+ dest.writeBundle(transientExtras);
+ if (clipData != null) {
+ dest.writeInt(1);
+ clipData.writeToParcel(dest, flags);
+ dest.writeInt(clipGrantFlags);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeStrongBinder(callback);
+ dest.writeInt(overrideDeadlineExpired ? 1 : 0);
+ dest.writeTypedArray(mTriggeredContentUris, flags);
+ dest.writeStringArray(mTriggeredContentAuthorities);
+ dest.writeInt(stopReason);
+ }
+
+ public static final Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
+ @Override
+ public JobParameters createFromParcel(Parcel in) {
+ return new JobParameters(in);
+ }
+
+ @Override
+ public JobParameters[] newArray(int size) {
+ return new JobParameters[size];
+ }
+ };
+}
diff --git a/android/app/job/JobScheduler.java b/android/app/job/JobScheduler.java
new file mode 100644
index 00000000..3868439f
--- /dev/null
+++ b/android/app/job/JobScheduler.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.job;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * This is an API for scheduling various types of jobs against the framework that will be executed
+ * in your application's own process.
+ * <p>
+ * See {@link android.app.job.JobInfo} for more description of the types of jobs that can be run
+ * and how to construct them. You will construct these JobInfo objects and pass them to the
+ * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the
+ * system will execute this job on your application's {@link android.app.job.JobService}.
+ * You identify which JobService is meant to execute the logic for your job when you create the
+ * JobInfo with
+ * {@link android.app.job.JobInfo.Builder#JobInfo.Builder(int,android.content.ComponentName)}.
+ * </p>
+ * <p>
+ * The framework will be intelligent about when you receive your callbacks, and attempt to batch
+ * and defer them as much as possible. Typically if you don't specify a deadline on your job, it
+ * can be run at any moment depending on the current state of the JobScheduler's internal queue,
+ * however it might be deferred as long as until the next time the device is connected to a power
+ * source.
+ * </p>
+ * <p>You do not
+ * instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService
+ * Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)}.
+ */
+@SystemService(Context.JOB_SCHEDULER_SERVICE)
+public abstract class JobScheduler {
+ /** @hide */
+ @IntDef(prefix = { "RESULT_" }, value = {
+ RESULT_FAILURE,
+ RESULT_SUCCESS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Result {}
+
+ /**
+ * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur
+ * if the run-time for your job is too short, or perhaps the system can't resolve the
+ * requisite {@link JobService} in your package.
+ */
+ public static final int RESULT_FAILURE = 0;
+ /**
+ * Returned from {@link #schedule(JobInfo)} if this job has been successfully scheduled.
+ */
+ public static final int RESULT_SUCCESS = 1;
+
+ /**
+ * Schedule a job to be executed. Will replace any currently scheduled job with the same
+ * ID with the new information in the {@link JobInfo}. If a job with the given ID is currently
+ * running, it will be stopped.
+ *
+ * @param job The job you wish scheduled. See
+ * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
+ * you can schedule.
+ * @return the result of the schedule request.
+ */
+ public abstract @Result int schedule(@NonNull JobInfo job);
+
+ /**
+ * Similar to {@link #schedule}, but allows you to enqueue work for a new <em>or existing</em>
+ * job. If a job with the same ID is already scheduled, it will be replaced with the
+ * new {@link JobInfo}, but any previously enqueued work will remain and be dispatched the
+ * next time it runs. If a job with the same ID is already running, the new work will be
+ * enqueued for it.
+ *
+ * <p>The work you enqueue is later retrieved through
+ * {@link JobParameters#dequeueWork() JobParameters.dequeueWork}. Be sure to see there
+ * about how to process work; the act of enqueueing work changes how you should handle the
+ * overall lifecycle of an executing job.</p>
+ *
+ * <p>It is strongly encouraged that you use the same {@link JobInfo} for all work you
+ * enqueue. This will allow the system to optimally schedule work along with any pending
+ * and/or currently running work. If the JobInfo changes from the last time the job was
+ * enqueued, the system will need to update the associated JobInfo, which can cause a disruption
+ * in execution. In particular, this can result in any currently running job that is processing
+ * previous work to be stopped and restarted with the new JobInfo.</p>
+ *
+ * <p>It is recommended that you avoid using
+ * {@link JobInfo.Builder#setExtras(PersistableBundle)} or
+ * {@link JobInfo.Builder#setTransientExtras(Bundle)} with a JobInfo you are using to
+ * enqueue work. The system will try to compare these extras with the previous JobInfo,
+ * but there are situations where it may get this wrong and count the JobInfo as changing.
+ * (That said, you should be relatively safe with a simple set of consistent data in these
+ * fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with
+ * work you are enqueue, since currently this will always be treated as a different JobInfo,
+ * even if the ClipData contents are exactly the same.</p>
+ *
+ * @param job The job you wish to enqueue work for. See
+ * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
+ * you can schedule.
+ * @param work New work to enqueue. This will be available later when the job starts running.
+ * @return the result of the enqueue request.
+ */
+ public abstract @Result int enqueue(@NonNull JobInfo job, @NonNull JobWorkItem work);
+
+ /**
+ *
+ * @param job The job to be scheduled.
+ * @param packageName The package on behalf of which the job is to be scheduled. This will be
+ * used to track battery usage and appIdleState.
+ * @param userId User on behalf of whom this job is to be scheduled.
+ * @param tag Debugging tag for dumps associated with this job (instead of the service class)
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public abstract @Result int scheduleAsPackage(@NonNull JobInfo job, @NonNull String packageName,
+ int userId, String tag);
+
+ /**
+ * Cancel a job that is pending in the JobScheduler.
+ * @param jobId unique identifier for this job. Obtain this value from the jobs returned by
+ * {@link #getAllPendingJobs()}.
+ */
+ public abstract void cancel(int jobId);
+
+ /**
+ * Cancel all jobs that have been registered with the JobScheduler by this package.
+ */
+ public abstract void cancelAll();
+
+ /**
+ * Retrieve all jobs for this package that are pending in the JobScheduler.
+ *
+ * @return a list of all the jobs registered by this package that have not
+ * yet been executed.
+ */
+ public abstract @NonNull List<JobInfo> getAllPendingJobs();
+
+ /**
+ * Retrieve a specific job for this package that is pending in the
+ * JobScheduler.
+ *
+ * @return job registered by this package that has not yet been executed.
+ */
+ public abstract @Nullable JobInfo getPendingJob(int jobId);
+}
diff --git a/android/app/job/JobService.java b/android/app/job/JobService.java
new file mode 100644
index 00000000..9096b47b
--- /dev/null
+++ b/android/app/job/JobService.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.job;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
+ * <p>This is the base class that handles asynchronous requests that were previously scheduled. You
+ * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where
+ * you will implement your job logic.</p>
+ * <p>This service executes each incoming job on a {@link android.os.Handler} running on your
+ * application's main thread. This means that you <b>must</b> offload your execution logic to
+ * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result
+ * in blocking any future callbacks from the JobManager - specifically
+ * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the
+ * scheduling requirements are no longer being met.</p>
+ */
+public abstract class JobService extends Service {
+ private static final String TAG = "JobService";
+
+ /**
+ * Job services must be protected with this permission:
+ *
+ * <pre class="prettyprint">
+ * &#60;service android:name="MyJobService"
+ * android:permission="android.permission.BIND_JOB_SERVICE" &#62;
+ * ...
+ * &#60;/service&#62;
+ * </pre>
+ *
+ * <p>If a job service is declared in the manifest but not protected with this
+ * permission, that service will be ignored by the OS.
+ */
+ public static final String PERMISSION_BIND =
+ "android.permission.BIND_JOB_SERVICE";
+
+ private JobServiceEngine mEngine;
+
+ /** @hide */
+ public final IBinder onBind(Intent intent) {
+ if (mEngine == null) {
+ mEngine = new JobServiceEngine(this) {
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ return JobService.this.onStartJob(params);
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return JobService.this.onStopJob(params);
+ }
+ };
+ }
+ return mEngine.getBinder();
+ }
+
+ /**
+ * Override this method with the callback logic for your job. Any such logic needs to be
+ * performed on a separate thread, as this function is executed on your application's main
+ * thread.
+ *
+ * @param params Parameters specifying info about this job, including the extras bundle you
+ * optionally provided at job-creation time.
+ * @return True if your service needs to process the work (on a separate thread). False if
+ * there's no more work to be done for this job.
+ */
+ public abstract boolean onStartJob(JobParameters params);
+
+ /**
+ * This method is called if the system has determined that you must stop execution of your job
+ * even before you've had a chance to call {@link #jobFinished(JobParameters, boolean)}.
+ *
+ * <p>This will happen if the requirements specified at schedule time are no longer met. For
+ * example you may have requested WiFi with
+ * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your
+ * job was executing the user toggled WiFi. Another example is if you had specified
+ * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its
+ * idle maintenance window. You are solely responsible for the behaviour of your application
+ * upon receipt of this message; your app will likely start to misbehave if you ignore it. One
+ * immediate repercussion is that the system will cease holding a wakelock for you.</p>
+ *
+ * @param params Parameters specifying info about this job.
+ * @return True to indicate to the JobManager whether you'd like to reschedule this job based
+ * on the retry criteria provided at job creation-time. False to drop the job. Regardless of
+ * the value returned, your job must stop executing.
+ */
+ public abstract boolean onStopJob(JobParameters params);
+
+ /**
+ * Call this to inform the JobManager you've finished executing. This can be called from any
+ * thread, as it will ultimately be run on your application's main thread. When the system
+ * receives this message it will release the wakelock being held.
+ * <p>
+ * You can specify post-execution behaviour to the scheduler here with
+ * <code>needsReschedule </code>. This will apply a back-off timer to your job based on
+ * the default, or what was set with
+ * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}. The original
+ * requirements are always honoured even for a backed-off job. Note that a job running in
+ * idle mode will not be backed-off. Instead what will happen is the job will be re-added
+ * to the queue and re-executed within a future idle maintenance window.
+ * </p>
+ *
+ * @param params Parameters specifying system-provided info about this job, this was given to
+ * your application in {@link #onStartJob(JobParameters)}.
+ * @param needsReschedule True if this job should be rescheduled according to the back-off
+ * criteria specified at schedule-time. False otherwise.
+ */
+ public final void jobFinished(JobParameters params, boolean needsReschedule) {
+ mEngine.jobFinished(params, needsReschedule);
+ }
+} \ No newline at end of file
diff --git a/android/app/job/JobServiceEngine.java b/android/app/job/JobServiceEngine.java
new file mode 100644
index 00000000..ab94da84
--- /dev/null
+++ b/android/app/job/JobServiceEngine.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.job;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Helper for implementing a {@link android.app.Service} that interacts with
+ * {@link JobScheduler}. This is not intended for use by regular applications, but
+ * allows frameworks built on top of the platform to create their own
+ * {@link android.app.Service} that interact with {@link JobScheduler} as well as
+ * add in additional functionality. If you just want to execute jobs normally, you
+ * should instead be looking at {@link JobService}.
+ */
+public abstract class JobServiceEngine {
+ private static final String TAG = "JobServiceEngine";
+
+ /**
+ * Identifier for a message that will result in a call to
+ * {@link #onStartJob(android.app.job.JobParameters)}.
+ */
+ private static final int MSG_EXECUTE_JOB = 0;
+ /**
+ * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}.
+ */
+ private static final int MSG_STOP_JOB = 1;
+ /**
+ * Message that the client has completed execution of this job.
+ */
+ private static final int MSG_JOB_FINISHED = 2;
+
+ private final IJobService mBinder;
+
+ /**
+ * Handler we post jobs to. Responsible for calling into the client logic, and handling the
+ * callback to the system.
+ */
+ JobHandler mHandler;
+
+ static final class JobInterface extends IJobService.Stub {
+ final WeakReference<JobServiceEngine> mService;
+
+ JobInterface(JobServiceEngine service) {
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void startJob(JobParameters jobParams) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams);
+ m.sendToTarget();
+ }
+ }
+
+ @Override
+ public void stopJob(JobParameters jobParams) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams);
+ m.sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Runs on application's main thread - callbacks are meant to offboard work to some other
+ * (app-specified) mechanism.
+ * @hide
+ */
+ class JobHandler extends Handler {
+ JobHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final JobParameters params = (JobParameters) msg.obj;
+ switch (msg.what) {
+ case MSG_EXECUTE_JOB:
+ try {
+ boolean workOngoing = JobServiceEngine.this.onStartJob(params);
+ ackStartMessage(params, workOngoing);
+ } catch (Exception e) {
+ Log.e(TAG, "Error while executing job: " + params.getJobId());
+ throw new RuntimeException(e);
+ }
+ break;
+ case MSG_STOP_JOB:
+ try {
+ boolean ret = JobServiceEngine.this.onStopJob(params);
+ ackStopMessage(params, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle onStopJob.", e);
+ throw new RuntimeException(e);
+ }
+ break;
+ case MSG_JOB_FINISHED:
+ final boolean needsReschedule = (msg.arg2 == 1);
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.jobFinished(params.getJobId(), needsReschedule);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error reporting job finish to system: binder has gone" +
+ "away.");
+ }
+ } else {
+ Log.e(TAG, "finishJob() called for a nonexistent job id.");
+ }
+ break;
+ default:
+ Log.e(TAG, "Unrecognised message received.");
+ break;
+ }
+ }
+
+ private void ackStartMessage(JobParameters params, boolean workOngoing) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeStartMessage(jobId, workOngoing);
+ } catch(RemoteException e) {
+ Log.e(TAG, "System unreachable for starting job.");
+ }
+ } else {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+ }
+
+ private void ackStopMessage(JobParameters params, boolean reschedule) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeStopMessage(jobId, reschedule);
+ } catch(RemoteException e) {
+ Log.e(TAG, "System unreachable for stopping job.");
+ }
+ } else {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new engine, ready for use.
+ *
+ * @param service The {@link Service} that is creating this engine and in which it will run.
+ */
+ public JobServiceEngine(Service service) {
+ mBinder = new JobInterface(this);
+ mHandler = new JobHandler(service.getMainLooper());
+ }
+
+ /**
+ * Retrieve the engine's IPC interface that should be returned by
+ * {@link Service#onBind(Intent)}.
+ */
+ public final IBinder getBinder() {
+ return mBinder.asBinder();
+ }
+
+ /**
+ * Engine's report that a job has started. See
+ * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information.
+ */
+ public abstract boolean onStartJob(JobParameters params);
+
+ /**
+ * Engine's report that a job has stopped. See
+ * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information.
+ */
+ public abstract boolean onStopJob(JobParameters params);
+
+ /**
+ * Call in to engine to report that a job has finished executing. See
+ * {@link JobService#jobFinished(JobParameters, boolean)} JobService.jobFinished} for more
+ * information.
+ */
+ public void jobFinished(JobParameters params, boolean needsReschedule) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
+ m.arg2 = needsReschedule ? 1 : 0;
+ m.sendToTarget();
+ }
+} \ No newline at end of file
diff --git a/android/app/job/JobWorkItem.java b/android/app/job/JobWorkItem.java
new file mode 100644
index 00000000..0eb0450e
--- /dev/null
+++ b/android/app/job/JobWorkItem.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.job;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A unit of work that can be enqueued for a job using
+ * {@link JobScheduler#enqueue JobScheduler.enqueue}. See
+ * {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details.
+ */
+final public class JobWorkItem implements Parcelable {
+ final Intent mIntent;
+ int mDeliveryCount;
+ int mWorkId;
+ Object mGrants;
+
+ /**
+ * Create a new piece of work, which can be submitted to
+ * {@link JobScheduler#enqueue JobScheduler.enqueue}.
+ *
+ * @param intent The general Intent describing this work.
+ */
+ public JobWorkItem(Intent intent) {
+ mIntent = intent;
+ }
+
+ /**
+ * Return the Intent associated with this work.
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Return the count of the number of times this work item has been delivered
+ * to the job. The value will be > 1 if it has been redelivered because the job
+ * was stopped or crashed while it had previously been delivered but before the
+ * job had called {@link JobParameters#completeWork JobParameters.completeWork} for it.
+ */
+ public int getDeliveryCount() {
+ return mDeliveryCount;
+ }
+
+ /**
+ * @hide
+ */
+ public void bumpDeliveryCount() {
+ mDeliveryCount++;
+ }
+
+ /**
+ * @hide
+ */
+ public void setWorkId(int id) {
+ mWorkId = id;
+ }
+
+ /**
+ * @hide
+ */
+ public int getWorkId() {
+ return mWorkId;
+ }
+
+ /**
+ * @hide
+ */
+ public void setGrants(Object grants) {
+ mGrants = grants;
+ }
+
+ /**
+ * @hide
+ */
+ public Object getGrants() {
+ return mGrants;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append("JobWorkItem{id=");
+ sb.append(mWorkId);
+ sb.append(" intent=");
+ sb.append(mIntent);
+ if (mDeliveryCount != 0) {
+ sb.append(" dcount=");
+ sb.append(mDeliveryCount);
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ if (mIntent != null) {
+ out.writeInt(1);
+ mIntent.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ out.writeInt(mDeliveryCount);
+ out.writeInt(mWorkId);
+ }
+
+ public static final Parcelable.Creator<JobWorkItem> CREATOR
+ = new Parcelable.Creator<JobWorkItem>() {
+ public JobWorkItem createFromParcel(Parcel in) {
+ return new JobWorkItem(in);
+ }
+
+ public JobWorkItem[] newArray(int size) {
+ return new JobWorkItem[size];
+ }
+ };
+
+ JobWorkItem(Parcel in) {
+ if (in.readInt() != 0) {
+ mIntent = Intent.CREATOR.createFromParcel(in);
+ } else {
+ mIntent = null;
+ }
+ mDeliveryCount = in.readInt();
+ mWorkId = in.readInt();
+ }
+}
diff --git a/android/app/timezone/Callback.java b/android/app/timezone/Callback.java
new file mode 100644
index 00000000..aea80380
--- /dev/null
+++ b/android/app/timezone/Callback.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback interface for receiving information about an async time zone operation.
+ * The methods will be called on your application's main thread.
+ *
+ * @hide
+ */
+public abstract class Callback {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_INSTALL_BAD_DISTRO_STRUCTURE,
+ ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION, ERROR_INSTALL_RULES_TOO_OLD,
+ ERROR_INSTALL_VALIDATION_ERROR})
+ public @interface AsyncResultCode {}
+
+ /**
+ * Indicates that an operation succeeded.
+ */
+ public static final int SUCCESS = 0;
+
+ /**
+ * Indicates an install / uninstall did not fully succeed for an unknown reason.
+ */
+ public static final int ERROR_UNKNOWN_FAILURE = 1;
+
+ /**
+ * Indicates an install failed because of a structural issue with the provided distro,
+ * e.g. it wasn't in the right format or the contents were structured incorrectly.
+ */
+ public static final int ERROR_INSTALL_BAD_DISTRO_STRUCTURE = 2;
+
+ /**
+ * Indicates an install failed because of a versioning issue with the provided distro,
+ * e.g. it was created for a different version of Android.
+ */
+ public static final int ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION = 3;
+
+ /**
+ * Indicates an install failed because the rules provided are too old for the device,
+ * e.g. the Android device shipped with a newer rules version.
+ */
+ public static final int ERROR_INSTALL_RULES_TOO_OLD = 4;
+
+ /**
+ * Indicates an install failed because the distro contents failed validation.
+ */
+ public static final int ERROR_INSTALL_VALIDATION_ERROR = 5;
+
+ /**
+ * Reports the result of an async time zone operation.
+ */
+ public abstract void onFinished(@AsyncResultCode int status);
+}
diff --git a/android/app/timezone/DistroFormatVersion.java b/android/app/timezone/DistroFormatVersion.java
new file mode 100644
index 00000000..be732e4c
--- /dev/null
+++ b/android/app/timezone/DistroFormatVersion.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Versioning information about a distro's format or a device's supported format.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ * <dt>majorVersion</dt>
+ * <dd>the major distro format version. Major versions differences are not compatible - e.g.
+ * 2 is not compatible with 1 or 3.</dd>
+ * <dt>minorVersion</dt>
+ * <dd>the minor distro format version. Minor versions should be backwards compatible iff the
+ * major versions match exactly, i.e. version 2.2 will be compatible with 2.1 devices but not
+ * 2.3 devices.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+public final class DistroFormatVersion implements Parcelable {
+
+ private final int mMajorVersion;
+ private final int mMinorVersion;
+
+ public DistroFormatVersion(int majorVersion, int minorVersion) {
+ mMajorVersion = Utils.validateVersion("major", majorVersion);
+ mMinorVersion = Utils.validateVersion("minor", minorVersion);
+ }
+
+ public static final Creator<DistroFormatVersion> CREATOR = new Creator<DistroFormatVersion>() {
+ public DistroFormatVersion createFromParcel(Parcel in) {
+ int majorVersion = in.readInt();
+ int minorVersion = in.readInt();
+ return new DistroFormatVersion(majorVersion, minorVersion);
+ }
+
+ public DistroFormatVersion[] newArray(int size) {
+ return new DistroFormatVersion[size];
+ }
+ };
+
+ public int getMajorVersion() {
+ return mMajorVersion;
+ }
+
+ public int getMinorVersion() {
+ return mMinorVersion;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mMajorVersion);
+ out.writeInt(mMinorVersion);
+ }
+
+ /**
+ * If this object describes a device's supported version and the parameter describes a distro's
+ * version, this method returns whether the device would accept the distro.
+ */
+ public boolean supports(DistroFormatVersion distroFormatVersion) {
+ return mMajorVersion == distroFormatVersion.mMajorVersion
+ && mMinorVersion <= distroFormatVersion.mMinorVersion;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DistroFormatVersion that = (DistroFormatVersion) o;
+
+ if (mMajorVersion != that.mMajorVersion) {
+ return false;
+ }
+ return mMinorVersion == that.mMinorVersion;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mMajorVersion;
+ result = 31 * result + mMinorVersion;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DistroFormatVersion{"
+ + "mMajorVersion=" + mMajorVersion
+ + ", mMinorVersion=" + mMinorVersion
+ + '}';
+ }
+}
diff --git a/android/app/timezone/DistroRulesVersion.java b/android/app/timezone/DistroRulesVersion.java
new file mode 100644
index 00000000..a6805946
--- /dev/null
+++ b/android/app/timezone/DistroRulesVersion.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import static android.app.timezone.Utils.validateRulesVersion;
+import static android.app.timezone.Utils.validateVersion;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Versioning information about a set of time zone rules.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ * <dt>rulesVersion</dt>
+ * <dd>the IANA rules version. e.g. "2017a"</dd>
+ * <dt>revision</dt>
+ * <dd>the revision for the rules. Allows there to be several revisions for a given IANA rules
+ * release. Numerically higher is newer.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+public final class DistroRulesVersion implements Parcelable {
+
+ private final String mRulesVersion;
+ private final int mRevision;
+
+ public DistroRulesVersion(String rulesVersion, int revision) {
+ mRulesVersion = validateRulesVersion("rulesVersion", rulesVersion);
+ mRevision = validateVersion("revision", revision);
+ }
+
+ public static final Creator<DistroRulesVersion> CREATOR = new Creator<DistroRulesVersion>() {
+ public DistroRulesVersion createFromParcel(Parcel in) {
+ String rulesVersion = in.readString();
+ int revision = in.readInt();
+ return new DistroRulesVersion(rulesVersion, revision);
+ }
+
+ public DistroRulesVersion[] newArray(int size) {
+ return new DistroRulesVersion[size];
+ }
+ };
+
+ public String getRulesVersion() {
+ return mRulesVersion;
+ }
+
+ public int getRevision() {
+ return mRevision;
+ }
+
+ /**
+ * Returns true if this DistroRulesVersion is older than the one supplied. It returns false if
+ * it is the same or newer. This method compares the {@code rulesVersion} and the
+ * {@code revision}.
+ */
+ public boolean isOlderThan(DistroRulesVersion distroRulesVersion) {
+ int rulesComparison = mRulesVersion.compareTo(distroRulesVersion.mRulesVersion);
+ if (rulesComparison < 0) {
+ return true;
+ }
+ if (rulesComparison > 0) {
+ return false;
+ }
+ return mRevision < distroRulesVersion.mRevision;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mRulesVersion);
+ out.writeInt(mRevision);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DistroRulesVersion that = (DistroRulesVersion) o;
+
+ if (mRevision != that.mRevision) {
+ return false;
+ }
+ return mRulesVersion.equals(that.mRulesVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mRulesVersion.hashCode();
+ result = 31 * result + mRevision;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "DistroRulesVersion{"
+ + "mRulesVersion='" + mRulesVersion + '\''
+ + ", mRevision='" + mRevision + '\''
+ + '}';
+ }
+
+ public String toDumpString() {
+ return mRulesVersion + "," + mRevision;
+ }
+}
diff --git a/android/app/timezone/RulesManager.java b/android/app/timezone/RulesManager.java
new file mode 100644
index 00000000..ad9b698a
--- /dev/null
+++ b/android/app/timezone/RulesManager.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * The interface through which a time zone update application interacts with the Android system
+ * to handle time zone rule updates.
+ *
+ * <p>This interface is intended for use with the default APK-based time zone rules update
+ * application but it can also be used by OEMs if that mechanism is turned off using configuration.
+ * All callers must possess the {@link android.Manifest.permission#UPDATE_TIME_ZONE_RULES} system
+ * permission.
+ *
+ * <p>When using the default mechanism, when properly configured the Android system will send a
+ * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with a
+ * {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} extra to the time zone rules updater application
+ * when it detects that it or the OEM's APK containing time zone rules data has been modified. The
+ * updater application is then responsible for calling one of
+ * {@link #requestInstall(ParcelFileDescriptor, byte[], Callback)},
+ * {@link #requestUninstall(byte[], Callback)} or
+ * {@link #requestNothing(byte[], boolean)}, indicating, respectively, whether a new time zone rules
+ * distro should be installed, the current distro should be uninstalled, or there is nothing to do
+ * (or that the correct operation could not be determined due to an error). In each case the updater
+ * must pass the {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} value it received from the intent
+ * back so the system in the {@code checkToken} parameter.
+ *
+ * <p>If OEMs want to handle their own time zone rules updates, perhaps via a server-side component
+ * rather than an APK, then they should disable the default triggering mechanism in config and are
+ * responsible for triggering their own update checks / installs / uninstalls. In this case the
+ * "check token" parameter can be left null and there is never any need to call
+ * {@link #requestNothing(byte[], boolean)}.
+ *
+ * <p>OEMs should not mix the default mechanism and their own as this could lead to conflicts and
+ * unnecessary checks being triggered.
+ *
+ * <p>Applications obtain this using {@link android.app.Activity#getSystemService(String)} with
+ * {@link Context#TIME_ZONE_RULES_MANAGER_SERVICE}.
+ * @hide
+ */
+public final class RulesManager {
+ private static final String TAG = "timezone.RulesManager";
+ private static final boolean DEBUG = false;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_OPERATION_IN_PROGRESS})
+ public @interface ResultCode {}
+
+ /**
+ * Indicates that an operation succeeded.
+ */
+ public static final int SUCCESS = 0;
+
+ /**
+ * Indicates that an install/uninstall cannot be initiated because there is one already in
+ * progress.
+ */
+ public static final int ERROR_OPERATION_IN_PROGRESS = 1;
+
+ /**
+ * Indicates an install / uninstall did not fully succeed for an unknown reason.
+ */
+ public static final int ERROR_UNKNOWN_FAILURE = 2;
+
+ private final Context mContext;
+ private final IRulesManager mIRulesManager;
+
+ public RulesManager(Context context) {
+ mContext = context;
+ mIRulesManager = IRulesManager.Stub.asInterface(
+ ServiceManager.getService(Context.TIME_ZONE_RULES_MANAGER_SERVICE));
+ }
+
+ /**
+ * Returns information about the current time zone rules state such as the IANA version of
+ * the system and any currently installed distro. This method is intended to allow clients to
+ * determine if the current state can be improved; for example by passing the information to a
+ * server that may provide a new distro for download.
+ */
+ public RulesState getRulesState() {
+ try {
+ logDebug("sIRulesManager.getRulesState()");
+ RulesState rulesState = mIRulesManager.getRulesState();
+ logDebug("sIRulesManager.getRulesState() returned " + rulesState);
+ return rulesState;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Requests installation of the supplied distro. The distro must have been checked for integrity
+ * by the caller or have been received via a trusted mechanism.
+ *
+ * @param distroFileDescriptor the file descriptor for the distro
+ * @param checkToken an optional token provided if the install was triggered in response to a
+ * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+ * @param callback the {@link Callback} to receive callbacks related to the installation
+ * @return {@link #SUCCESS} if the installation will be attempted
+ */
+ @ResultCode
+ public int requestInstall(
+ ParcelFileDescriptor distroFileDescriptor, byte[] checkToken, Callback callback)
+ throws IOException {
+
+ ICallback iCallback = new CallbackWrapper(mContext, callback);
+ try {
+ logDebug("sIRulesManager.requestInstall()");
+ return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Requests uninstallation of the currently installed distro (leaving the device with no
+ * distro installed).
+ *
+ * @param checkToken an optional token provided if the uninstall was triggered in response to a
+ * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+ * @param callback the {@link Callback} to receive callbacks related to the uninstall
+ * @return {@link #SUCCESS} if the uninstallation will be attempted
+ */
+ @ResultCode
+ public int requestUninstall(byte[] checkToken, Callback callback) {
+ ICallback iCallback = new CallbackWrapper(mContext, callback);
+ try {
+ logDebug("sIRulesManager.requestUninstall()");
+ return mIRulesManager.requestUninstall(checkToken, iCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /*
+ * We wrap incoming binder calls with a private class implementation that
+ * redirects them into main-thread actions. This serializes the backup
+ * progress callbacks nicely within the usual main-thread lifecycle pattern.
+ */
+ private class CallbackWrapper extends ICallback.Stub {
+ final Handler mHandler;
+ final Callback mCallback;
+
+ CallbackWrapper(Context context, Callback callback) {
+ mCallback = callback;
+ mHandler = new Handler(context.getMainLooper());
+ }
+
+ // Binder calls into this object just enqueue on the main-thread handler
+ @Override
+ public void onFinished(int status) {
+ logDebug("mCallback.onFinished(status), status=" + status);
+ mHandler.post(() -> mCallback.onFinished(status));
+ }
+ }
+
+ /**
+ * Requests the system does not modify the currently installed time zone distro, if any. This
+ * method records the fact that a time zone check operation triggered by the system is now
+ * complete and there was nothing to do. The token passed should be the one presented when the
+ * check was triggered.
+ *
+ * <p>Note: Passing {@code success == false} may result in more checks being triggered. Clients
+ * should be careful not to pass false if the failure is unlikely to resolve by itself.
+ *
+ * @param checkToken an optional token provided if the install was triggered in response to a
+ * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
+ * @param succeeded true if the check was successful, false if it was not successful but may
+ * succeed if it is retried
+ */
+ public void requestNothing(byte[] checkToken, boolean succeeded) {
+ try {
+ logDebug("sIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
+ mIRulesManager.requestNothing(checkToken, succeeded);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ static void logDebug(String msg) {
+ if (DEBUG) {
+ Log.v(TAG, msg);
+ }
+ }
+}
diff --git a/android/app/timezone/RulesState.java b/android/app/timezone/RulesState.java
new file mode 100644
index 00000000..ec247ebf
--- /dev/null
+++ b/android/app/timezone/RulesState.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import static android.app.timezone.Utils.validateConditionalNull;
+import static android.app.timezone.Utils.validateNotNull;
+import static android.app.timezone.Utils.validateRulesVersion;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Description of the state of time zone rules on a device.
+ *
+ * <p>The following properties are included:
+ * <dl>
+ * <dt>systemRulesVersion</dt>
+ * <dd>the IANA rules version that shipped with the OS. Always present. e.g. "2017a".</dd>
+ * <dt>distroFormatVersionSupported</dt>
+ * <dd>the distro format version supported by this device. Always present.</dd>
+ * <dt>operationInProgress</dt>
+ * <dd>{@code true} if there is an install / uninstall operation currently happening.</dd>
+ * <dt>stagedOperationType</dt>
+ * <dd>one of {@link #STAGED_OPERATION_UNKNOWN}, {@link #STAGED_OPERATION_NONE},
+ * {@link #STAGED_OPERATION_UNINSTALL} and {@link #STAGED_OPERATION_INSTALL} indicating whether
+ * there is a currently staged time zone distro operation. {@link #STAGED_OPERATION_UNKNOWN} is
+ * used when {@link #isOperationInProgress()} is {@code true}. Staged operations currently
+ * require a reboot to become active.</dd>
+ * <dt>stagedDistroRulesVersion</dt>
+ * <dd>[present if distroStagedState == STAGED_STATE_INSTALL], the rules version of the distro
+ * currently staged for installation.</dd>
+ * <dt>distroStatus</dt>
+ * <dd>{@link #DISTRO_STATUS_INSTALLED} if there is a time zone distro installed and active,
+ * {@link #DISTRO_STATUS_NONE} if there is no active installed distro.
+ * {@link #DISTRO_STATUS_UNKNOWN} is used when {@link #isOperationInProgress()} is {@code true}.
+ * </dd>
+ * <dt>installedDistroRulesVersion</dt>
+ * <dd>[present if distroStatus == {@link #DISTRO_STATUS_INSTALLED}], the rules version of the
+ * installed and active distro.</dd>
+ * </dl>
+ *
+ * @hide
+ */
+public final class RulesState implements Parcelable {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STAGED_OPERATION_UNKNOWN,
+ STAGED_OPERATION_NONE,
+ STAGED_OPERATION_UNINSTALL,
+ STAGED_OPERATION_INSTALL })
+ private @interface StagedOperationType {}
+
+ /** Staged state could not be determined. */
+ public static final int STAGED_OPERATION_UNKNOWN = 0;
+ /** Nothing is staged. */
+ public static final int STAGED_OPERATION_NONE = 1;
+ /** An uninstall is staged. */
+ public static final int STAGED_OPERATION_UNINSTALL = 2;
+ /** An install is staged. */
+ public static final int STAGED_OPERATION_INSTALL = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DISTRO_STATUS_UNKNOWN,
+ DISTRO_STATUS_NONE,
+ DISTRO_STATUS_INSTALLED })
+ private @interface DistroStatus {}
+
+ /** The current distro status could not be determined. */
+ public static final int DISTRO_STATUS_UNKNOWN = 0;
+ /** There is no active installed time zone distro. */
+ public static final int DISTRO_STATUS_NONE = 1;
+ /** The is an active, installed time zone distro. */
+ public static final int DISTRO_STATUS_INSTALLED = 2;
+
+ private static final byte BYTE_FALSE = 0;
+ private static final byte BYTE_TRUE = 1;
+
+ private final String mSystemRulesVersion;
+ private final DistroFormatVersion mDistroFormatVersionSupported;
+ private final boolean mOperationInProgress;
+ @StagedOperationType private final int mStagedOperationType;
+ @Nullable private final DistroRulesVersion mStagedDistroRulesVersion;
+ @DistroStatus private final int mDistroStatus;
+ @Nullable private final DistroRulesVersion mInstalledDistroRulesVersion;
+
+ public RulesState(String systemRulesVersion, DistroFormatVersion distroFormatVersionSupported,
+ boolean operationInProgress,
+ @StagedOperationType int stagedOperationType,
+ @Nullable DistroRulesVersion stagedDistroRulesVersion,
+ @DistroStatus int distroStatus,
+ @Nullable DistroRulesVersion installedDistroRulesVersion) {
+ this.mSystemRulesVersion = validateRulesVersion("systemRulesVersion", systemRulesVersion);
+ this.mDistroFormatVersionSupported =
+ validateNotNull("distroFormatVersionSupported", distroFormatVersionSupported);
+ this.mOperationInProgress = operationInProgress;
+
+ if (operationInProgress && stagedOperationType != STAGED_OPERATION_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "stagedOperationType != STAGED_OPERATION_UNKNOWN");
+ }
+ this.mStagedOperationType = validateStagedOperation(stagedOperationType);
+ this.mStagedDistroRulesVersion = validateConditionalNull(
+ mStagedOperationType == STAGED_OPERATION_INSTALL /* requireNotNull */,
+ "stagedDistroRulesVersion", stagedDistroRulesVersion);
+
+ if (operationInProgress && distroStatus != DISTRO_STATUS_UNKNOWN) {
+ throw new IllegalArgumentException("distroInstalled != DISTRO_STATUS_UNKNOWN");
+ }
+ this.mDistroStatus = validateDistroStatus(distroStatus);
+ this.mInstalledDistroRulesVersion = validateConditionalNull(
+ mDistroStatus == DISTRO_STATUS_INSTALLED/* requireNotNull */,
+ "installedDistroRulesVersion", installedDistroRulesVersion);
+ }
+
+ public String getSystemRulesVersion() {
+ return mSystemRulesVersion;
+ }
+
+ public boolean isOperationInProgress() {
+ return mOperationInProgress;
+ }
+
+ public @StagedOperationType int getStagedOperationType() {
+ return mStagedOperationType;
+ }
+
+ /**
+ * Returns the staged rules version when {@link #getStagedOperationType()} is
+ * {@link #STAGED_OPERATION_INSTALL}.
+ */
+ public @Nullable DistroRulesVersion getStagedDistroRulesVersion() {
+ return mStagedDistroRulesVersion;
+ }
+
+ public @DistroStatus int getDistroStatus() {
+ return mDistroStatus;
+ }
+
+ /**
+ * Returns the installed rules version when {@link #getDistroStatus()} is
+ * {@link #DISTRO_STATUS_INSTALLED}.
+ */
+ public @Nullable DistroRulesVersion getInstalledDistroRulesVersion() {
+ return mInstalledDistroRulesVersion;
+ }
+
+ /**
+ * Returns true if a distro in the specified format is supported on this device.
+ */
+ public boolean isDistroFormatVersionSupported(DistroFormatVersion distroFormatVersion) {
+ return mDistroFormatVersionSupported.supports(distroFormatVersion);
+ }
+
+ /**
+ * Returns true if the system image data files contain IANA rules data that are newer than the
+ * distro IANA rules version supplied, i.e. true when the version specified would be "worse"
+ * than the one that is in the system image. Returns false if the system image version is the
+ * same or older, i.e. false when the version specified would be "better" than the one that is
+ * in the system image.
+ */
+ public boolean isSystemVersionNewerThan(DistroRulesVersion distroRulesVersion) {
+ return mSystemRulesVersion.compareTo(distroRulesVersion.getRulesVersion()) > 0;
+ }
+
+ public static final Parcelable.Creator<RulesState> CREATOR =
+ new Parcelable.Creator<RulesState>() {
+ public RulesState createFromParcel(Parcel in) {
+ return RulesState.createFromParcel(in);
+ }
+
+ public RulesState[] newArray(int size) {
+ return new RulesState[size];
+ }
+ };
+
+ private static RulesState createFromParcel(Parcel in) {
+ String systemRulesVersion = in.readString();
+ DistroFormatVersion distroFormatVersionSupported = in.readParcelable(null);
+ boolean operationInProgress = in.readByte() == BYTE_TRUE;
+ int distroStagedState = in.readByte();
+ DistroRulesVersion stagedDistroRulesVersion = in.readParcelable(null);
+ int installedDistroStatus = in.readByte();
+ DistroRulesVersion installedDistroRulesVersion = in.readParcelable(null);
+ return new RulesState(systemRulesVersion, distroFormatVersionSupported, operationInProgress,
+ distroStagedState, stagedDistroRulesVersion,
+ installedDistroStatus, installedDistroRulesVersion);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mSystemRulesVersion);
+ out.writeParcelable(mDistroFormatVersionSupported, 0);
+ out.writeByte(mOperationInProgress ? BYTE_TRUE : BYTE_FALSE);
+ out.writeByte((byte) mStagedOperationType);
+ out.writeParcelable(mStagedDistroRulesVersion, 0);
+ out.writeByte((byte) mDistroStatus);
+ out.writeParcelable(mInstalledDistroRulesVersion, 0);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ RulesState that = (RulesState) o;
+
+ if (mOperationInProgress != that.mOperationInProgress) {
+ return false;
+ }
+ if (mStagedOperationType != that.mStagedOperationType) {
+ return false;
+ }
+ if (mDistroStatus != that.mDistroStatus) {
+ return false;
+ }
+ if (!mSystemRulesVersion.equals(that.mSystemRulesVersion)) {
+ return false;
+ }
+ if (!mDistroFormatVersionSupported.equals(that.mDistroFormatVersionSupported)) {
+ return false;
+ }
+ if (mStagedDistroRulesVersion != null ? !mStagedDistroRulesVersion
+ .equals(that.mStagedDistroRulesVersion) : that.mStagedDistroRulesVersion != null) {
+ return false;
+ }
+ return mInstalledDistroRulesVersion != null ? mInstalledDistroRulesVersion
+ .equals(that.mInstalledDistroRulesVersion)
+ : that.mInstalledDistroRulesVersion == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mSystemRulesVersion.hashCode();
+ result = 31 * result + mDistroFormatVersionSupported.hashCode();
+ result = 31 * result + (mOperationInProgress ? 1 : 0);
+ result = 31 * result + mStagedOperationType;
+ result = 31 * result + (mStagedDistroRulesVersion != null ? mStagedDistroRulesVersion
+ .hashCode()
+ : 0);
+ result = 31 * result + mDistroStatus;
+ result = 31 * result + (mInstalledDistroRulesVersion != null ? mInstalledDistroRulesVersion
+ .hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RulesState{"
+ + "mSystemRulesVersion='" + mSystemRulesVersion + '\''
+ + ", mDistroFormatVersionSupported=" + mDistroFormatVersionSupported
+ + ", mOperationInProgress=" + mOperationInProgress
+ + ", mStagedOperationType=" + mStagedOperationType
+ + ", mStagedDistroRulesVersion=" + mStagedDistroRulesVersion
+ + ", mDistroStatus=" + mDistroStatus
+ + ", mInstalledDistroRulesVersion=" + mInstalledDistroRulesVersion
+ + '}';
+ }
+
+ private static int validateStagedOperation(int stagedOperationType) {
+ if (stagedOperationType < STAGED_OPERATION_UNKNOWN
+ || stagedOperationType > STAGED_OPERATION_INSTALL) {
+ throw new IllegalArgumentException("Unknown operation type=" + stagedOperationType);
+ }
+ return stagedOperationType;
+ }
+
+ private static int validateDistroStatus(int distroStatus) {
+ if (distroStatus < DISTRO_STATUS_UNKNOWN || distroStatus > DISTRO_STATUS_INSTALLED) {
+ throw new IllegalArgumentException("Unknown distro status=" + distroStatus);
+ }
+ return distroStatus;
+ }
+}
diff --git a/android/app/timezone/RulesUpdaterContract.java b/android/app/timezone/RulesUpdaterContract.java
new file mode 100644
index 00000000..9c62f46b
--- /dev/null
+++ b/android/app/timezone/RulesUpdaterContract.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timezone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+
+/**
+ * Constants related to the contract between the Android system and the privileged time zone updater
+ * application.
+ *
+ * @hide
+ */
+public final class RulesUpdaterContract {
+
+ /**
+ * The system permission possessed by the Android system that allows it to trigger time zone
+ * update checks. The updater should be configured to require this permission when registering
+ * for {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intents.
+ */
+ public static final String TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION =
+ android.Manifest.permission.TRIGGER_TIME_ZONE_RULES_CHECK;
+
+ /**
+ * The system permission possessed by the time zone rules updater app that allows it to update
+ * device time zone rules. The Android system requires this permission for calls made to
+ * {@link RulesManager}.
+ */
+ public static final String UPDATE_TIME_ZONE_RULES_PERMISSION =
+ android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
+
+ /**
+ * The action of the intent that the Android system will broadcast. The intent will be targeted
+ * at the configured updater application's package meaning the term "broadcast" only loosely
+ * applies.
+ */
+ public static final String ACTION_TRIGGER_RULES_UPDATE_CHECK =
+ "android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK";
+
+ /**
+ * The extra containing the {@code byte[]} that should be passed to
+ * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback)},
+ * {@link RulesManager#requestUninstall(byte[], Callback)} and
+ * {@link RulesManager#requestNothing(byte[], boolean)} methods when the
+ * {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intent has been processed.
+ */
+ public static final String EXTRA_CHECK_TOKEN =
+ "android.intent.extra.timezone.CHECK_TOKEN";
+
+ /**
+ * Creates an intent that would trigger a time zone rules update check.
+ */
+ public static Intent createUpdaterIntent(String updaterPackageName) {
+ Intent intent = new Intent(RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK);
+ intent.setPackage(updaterPackageName);
+ intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+ return intent;
+ }
+
+ /**
+ * Broadcasts an {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with the
+ * {@link #EXTRA_CHECK_TOKEN} that triggers an update check, including the required receiver
+ * permission.
+ */
+ public static void sendBroadcast(Context context, String updaterAppPackageName,
+ byte[] checkTokenBytes) {
+ Intent intent = createUpdaterIntent(updaterAppPackageName);
+ intent.putExtra(EXTRA_CHECK_TOKEN, checkTokenBytes);
+ context.sendBroadcastAsUser(
+ intent,
+ UserHandle.of(UserHandle.myUserId()),
+ RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION);
+ }
+}
diff --git a/android/app/timezone/Utils.java b/android/app/timezone/Utils.java
new file mode 100644
index 00000000..8dd3fb71
--- /dev/null
+++ b/android/app/timezone/Utils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app.timezone;
+
+/**
+ * Shared code for android.app.timezone classes.
+ */
+final class Utils {
+ private Utils() {}
+
+ static int validateVersion(String type, int version) {
+ if (version < 0 || version > 999) {
+ throw new IllegalArgumentException("Invalid " + type + " version=" + version);
+ }
+ return version;
+ }
+
+ static String validateRulesVersion(String type, String rulesVersion) {
+ validateNotNull(type, rulesVersion);
+
+ if (rulesVersion.isEmpty()) {
+ throw new IllegalArgumentException(type + " must not be empty");
+ }
+ return rulesVersion;
+ }
+
+ /** Validates that {@code object} is not null. Always returns {@code object}. */
+ static <T> T validateNotNull(String type, T object) {
+ if (object == null) {
+ throw new NullPointerException(type + " == null");
+ }
+ return object;
+ }
+
+ /**
+ * If {@code requireNotNull} is {@code true} calls {@link #validateNotNull(String, Object)},
+ * and {@link #validateNull(String, Object)} otherwise. Returns {@code object}.
+ */
+ static <T> T validateConditionalNull(boolean requireNotNull, String type, T object) {
+ if (requireNotNull) {
+ return validateNotNull(type, object);
+ } else {
+ return validateNull(type, object);
+ }
+ }
+
+ /** Validates that {@code object} is null. Always returns null. */
+ static <T> T validateNull(String type, T object) {
+ if (object != null) {
+ throw new IllegalArgumentException(type + " != null");
+ }
+ return null;
+ }
+}
diff --git a/android/app/trust/TrustManager.java b/android/app/trust/TrustManager.java
new file mode 100644
index 00000000..852cb8e0
--- /dev/null
+++ b/android/app/trust/TrustManager.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.app.trust;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+/**
+ * See {@link com.android.server.trust.TrustManagerService}
+ * @hide
+ */
+@SystemService(Context.TRUST_SERVICE)
+public class TrustManager {
+
+ private static final int MSG_TRUST_CHANGED = 1;
+ private static final int MSG_TRUST_MANAGED_CHANGED = 2;
+
+ private static final String TAG = "TrustManager";
+ private static final String DATA_FLAGS = "initiatedByUser";
+
+ private final ITrustManager mService;
+ private final ArrayMap<TrustListener, ITrustListener> mTrustListeners;
+
+ public TrustManager(IBinder b) {
+ mService = ITrustManager.Stub.asInterface(b);
+ mTrustListeners = new ArrayMap<TrustListener, ITrustListener>();
+ }
+
+ /**
+ * Changes the lock status for the given user. This is only applicable to Managed Profiles,
+ * other users should be handled by Keyguard.
+ *
+ * @param userId The id for the user to be locked/unlocked.
+ * @param locked The value for that user's locked state.
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE)
+ public void setDeviceLockedForUser(int userId, boolean locked) {
+ try {
+ mService.setDeviceLockedForUser(userId, locked);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reports that user {@param userId} has tried to unlock the device.
+ *
+ * @param successful if true, the unlock attempt was successful.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ */
+ public void reportUnlockAttempt(boolean successful, int userId) {
+ try {
+ mService.reportUnlockAttempt(successful, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reports that user {@param userId} has entered a temporary device lockout.
+ *
+ * This generally occurs when the user has unsuccessfully tried to unlock the device too many
+ * times. The user will then be unable to unlock the device until a set amount of time has
+ * elapsed.
+ *
+ * @param timeout The amount of time that needs to elapse, in milliseconds, until the user may
+ * attempt to unlock the device again.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ */
+ public void reportUnlockLockout(int timeoutMs, int userId) {
+ try {
+ mService.reportUnlockLockout(timeoutMs, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reports that the list of enabled trust agents changed for user {@param userId}.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ */
+ public void reportEnabledTrustAgentsChanged(int userId) {
+ try {
+ mService.reportEnabledTrustAgentsChanged(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reports that the visibility of the keyguard has changed.
+ *
+ * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
+ */
+ public void reportKeyguardShowingChanged() {
+ try {
+ mService.reportKeyguardShowingChanged();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers a listener for trust events.
+ *
+ * Requires the {@link android.Manifest.permission#TRUST_LISTENER} permission.
+ */
+ public void registerTrustListener(final TrustListener trustListener) {
+ try {
+ ITrustListener.Stub iTrustListener = new ITrustListener.Stub() {
+ @Override
+ public void onTrustChanged(boolean enabled, int userId, int flags) {
+ Message m = mHandler.obtainMessage(MSG_TRUST_CHANGED, (enabled ? 1 : 0), userId,
+ trustListener);
+ if (flags != 0) {
+ m.getData().putInt(DATA_FLAGS, flags);
+ }
+ m.sendToTarget();
+ }
+
+ @Override
+ public void onTrustManagedChanged(boolean managed, int userId) {
+ mHandler.obtainMessage(MSG_TRUST_MANAGED_CHANGED, (managed ? 1 : 0), userId,
+ trustListener).sendToTarget();
+ }
+ };
+ mService.registerTrustListener(iTrustListener);
+ mTrustListeners.put(trustListener, iTrustListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregisters a listener for trust events.
+ *
+ * Requires the {@link android.Manifest.permission#TRUST_LISTENER} permission.
+ */
+ public void unregisterTrustListener(final TrustListener trustListener) {
+ ITrustListener iTrustListener = mTrustListeners.remove(trustListener);
+ if (iTrustListener != null) {
+ try {
+ mService.unregisterTrustListener(iTrustListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * @return whether {@param userId} has enabled and configured trust agents. Ignores short-term
+ * unavailability of trust due to {@link LockPatternUtils.StrongAuthTracker}.
+ */
+ @RequiresPermission(android.Manifest.permission.TRUST_LISTENER)
+ public boolean isTrustUsuallyManaged(int userId) {
+ try {
+ return mService.isTrustUsuallyManaged(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Updates the trust state for the user due to the user unlocking via fingerprint.
+ * Should only be called if user authenticated via fingerprint and bouncer can be skipped.
+ * @param userId
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE)
+ public void unlockedByFingerprintForUser(int userId) {
+ try {
+ mService.unlockedByFingerprintForUser(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Clears authenticated fingerprints for all users.
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE)
+ public void clearAllFingerprints() {
+ try {
+ mService.clearAllFingerprints();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_TRUST_CHANGED:
+ int flags = msg.peekData() != null ? msg.peekData().getInt(DATA_FLAGS) : 0;
+ ((TrustListener)msg.obj).onTrustChanged(msg.arg1 != 0, msg.arg2, flags);
+ break;
+ case MSG_TRUST_MANAGED_CHANGED:
+ ((TrustListener)msg.obj).onTrustManagedChanged(msg.arg1 != 0, msg.arg2);
+ }
+ }
+ };
+
+ public interface TrustListener {
+
+ /**
+ * Reports that the trust state has changed.
+ * @param enabled if true, the system believes the environment to be trusted.
+ * @param userId the user, for which the trust changed.
+ * @param flags flags specified by the trust agent when granting trust. See
+ * {@link android.service.trust.TrustAgentService#grantTrust(CharSequence, long, int)
+ * TrustAgentService.grantTrust(CharSequence, long, int)}.
+ */
+ void onTrustChanged(boolean enabled, int userId, int flags);
+
+ /**
+ * Reports that whether trust is managed has changed
+ * @param enabled if true, at least one trust agent is managing trust.
+ * @param userId the user, for which the state changed.
+ */
+ void onTrustManagedChanged(boolean enabled, int userId);
+ }
+}
diff --git a/android/app/usage/CacheQuotaHint.java b/android/app/usage/CacheQuotaHint.java
new file mode 100644
index 00000000..1d5c2b05
--- /dev/null
+++ b/android/app/usage/CacheQuotaHint.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * CacheQuotaHint represents a triplet of a uid, the volume UUID it is stored upon, and
+ * its usage stats. When processed, it obtains a cache quota as defined by the system which
+ * allows apps to understand how much cache to use.
+ * {@hide}
+ */
+@SystemApi
+public final class CacheQuotaHint implements Parcelable {
+ public static final long QUOTA_NOT_SET = -1;
+ private final String mUuid;
+ private final int mUid;
+ private final UsageStats mUsageStats;
+ private final long mQuota;
+
+ /**
+ * Create a new request.
+ * @param builder A builder for this object.
+ */
+ public CacheQuotaHint(Builder builder) {
+ this.mUuid = builder.mUuid;
+ this.mUid = builder.mUid;
+ this.mUsageStats = builder.mUsageStats;
+ this.mQuota = builder.mQuota;
+ }
+
+ public String getVolumeUuid() {
+ return mUuid;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public long getQuota() {
+ return mQuota;
+ }
+
+ public UsageStats getUsageStats() {
+ return mUsageStats;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mUuid);
+ dest.writeInt(mUid);
+ dest.writeLong(mQuota);
+ dest.writeParcelable(mUsageStats, 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof CacheQuotaHint) {
+ final CacheQuotaHint other = (CacheQuotaHint) o;
+ return Objects.equals(mUuid, other.mUuid)
+ && Objects.equals(mUsageStats, other.mUsageStats)
+ && mUid == other.mUid && mQuota == other.mQuota;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.mUuid, this.mUid, this.mUsageStats, this.mQuota);
+ }
+
+ public static final class Builder {
+ private String mUuid;
+ private int mUid;
+ private UsageStats mUsageStats;
+ private long mQuota;
+
+ public Builder() {
+ }
+
+ public Builder(CacheQuotaHint hint) {
+ setVolumeUuid(hint.getVolumeUuid());
+ setUid(hint.getUid());
+ setUsageStats(hint.getUsageStats());
+ setQuota(hint.getQuota());
+ }
+
+ public @NonNull Builder setVolumeUuid(@Nullable String uuid) {
+ mUuid = uuid;
+ return this;
+ }
+
+ public @NonNull Builder setUid(int uid) {
+ Preconditions.checkArgumentNonnegative(uid, "Proposed uid was negative.");
+ mUid = uid;
+ return this;
+ }
+
+ public @NonNull Builder setUsageStats(@Nullable UsageStats stats) {
+ mUsageStats = stats;
+ return this;
+ }
+
+ public @NonNull Builder setQuota(long quota) {
+ Preconditions.checkArgument((quota >= QUOTA_NOT_SET));
+ mQuota = quota;
+ return this;
+ }
+
+ public @NonNull CacheQuotaHint build() {
+ return new CacheQuotaHint(this);
+ }
+ }
+
+ public static final Parcelable.Creator<CacheQuotaHint> CREATOR =
+ new Creator<CacheQuotaHint>() {
+ @Override
+ public CacheQuotaHint createFromParcel(Parcel in) {
+ final Builder builder = new Builder();
+ return builder.setVolumeUuid(in.readString())
+ .setUid(in.readInt())
+ .setQuota(in.readLong())
+ .setUsageStats(in.readParcelable(UsageStats.class.getClassLoader()))
+ .build();
+ }
+
+ @Override
+ public CacheQuotaHint[] newArray(int size) {
+ return new CacheQuotaHint[size];
+ }
+ };
+}
diff --git a/android/app/usage/CacheQuotaService.java b/android/app/usage/CacheQuotaService.java
new file mode 100644
index 00000000..b9430ab9
--- /dev/null
+++ b/android/app/usage/CacheQuotaService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.usage.ICacheQuotaService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.List;
+
+/**
+ * CacheQuoteService defines a service which accepts cache quota requests and processes them,
+ * thereby filling out how much quota each request deserves.
+ * {@hide}
+ */
+@SystemApi
+public abstract class CacheQuotaService extends Service {
+ private static final String TAG = "CacheQuotaService";
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE = "android.app.usage.CacheQuotaService";
+
+ /** {@hide} **/
+ public static final String REQUEST_LIST_KEY = "requests";
+
+ private CacheQuotaServiceWrapper mWrapper;
+ private Handler mHandler;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new CacheQuotaServiceWrapper();
+ mHandler = new ServiceHandler(getMainLooper());
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Processes the cache quota list upon receiving a list of requests.
+ * @param requests A list of cache quotas to fulfill.
+ * @return A completed list of cache quota requests.
+ */
+ public abstract List<CacheQuotaHint> onComputeCacheQuotaHints(
+ List<CacheQuotaHint> requests);
+
+ private final class CacheQuotaServiceWrapper extends ICacheQuotaService.Stub {
+ @Override
+ public void computeCacheQuotaHints(
+ RemoteCallback callback, List<CacheQuotaHint> requests) {
+ final Pair<RemoteCallback, List<CacheQuotaHint>> pair =
+ Pair.create(callback, requests);
+ Message msg = mHandler.obtainMessage(ServiceHandler.MSG_SEND_LIST, pair);
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ private final class ServiceHandler extends Handler {
+ public static final int MSG_SEND_LIST = 1;
+
+ public ServiceHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int action = msg.what;
+ switch (action) {
+ case MSG_SEND_LIST:
+ final Pair<RemoteCallback, List<CacheQuotaHint>> pair =
+ (Pair<RemoteCallback, List<CacheQuotaHint>>) msg.obj;
+ List<CacheQuotaHint> processed = onComputeCacheQuotaHints(pair.second);
+ final Bundle data = new Bundle();
+ data.putParcelableList(REQUEST_LIST_KEY, processed);
+
+ final RemoteCallback callback = pair.first;
+ callback.sendResult(data);
+ break;
+ default:
+ Log.w(TAG, "Handling unknown message: " + action);
+ }
+ }
+ }
+}
diff --git a/android/app/usage/ConfigurationStats.java b/android/app/usage/ConfigurationStats.java
new file mode 100644
index 00000000..080216ce
--- /dev/null
+++ b/android/app/usage/ConfigurationStats.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package android.app.usage;
+
+import android.content.res.Configuration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the usage statistics of a device {@link android.content.res.Configuration} for a
+ * specific time range.
+ */
+public final class ConfigurationStats implements Parcelable {
+
+ /**
+ * {@hide}
+ */
+ public Configuration mConfiguration;
+
+ /**
+ * {@hide}
+ */
+ public long mBeginTimeStamp;
+
+ /**
+ * {@hide}
+ */
+ public long mEndTimeStamp;
+
+ /**
+ * {@hide}
+ */
+ public long mLastTimeActive;
+
+ /**
+ * {@hide}
+ */
+ public long mTotalTimeActive;
+
+ /**
+ * {@hide}
+ */
+ public int mActivationCount;
+
+ /**
+ * {@hide}
+ */
+ public ConfigurationStats() {
+ }
+
+ public ConfigurationStats(ConfigurationStats stats) {
+ mConfiguration = stats.mConfiguration;
+ mBeginTimeStamp = stats.mBeginTimeStamp;
+ mEndTimeStamp = stats.mEndTimeStamp;
+ mLastTimeActive = stats.mLastTimeActive;
+ mTotalTimeActive = stats.mTotalTimeActive;
+ mActivationCount = stats.mActivationCount;
+ }
+
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Get the beginning of the time range this {@link ConfigurationStats} represents,
+ * measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getFirstTimeStamp() {
+ return mBeginTimeStamp;
+ }
+
+ /**
+ * Get the end of the time range this {@link ConfigurationStats} represents,
+ * measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getLastTimeStamp() {
+ return mEndTimeStamp;
+ }
+
+ /**
+ * Get the last time this configuration was active, measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getLastTimeActive() {
+ return mLastTimeActive;
+ }
+
+ /**
+ * Get the total time this configuration was active, measured in milliseconds.
+ */
+ public long getTotalTimeActive() {
+ return mTotalTimeActive;
+ }
+
+ /**
+ * Get the number of times this configuration was active.
+ */
+ public int getActivationCount() {
+ return mActivationCount;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mConfiguration != null) {
+ dest.writeInt(1);
+ mConfiguration.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+
+ dest.writeLong(mBeginTimeStamp);
+ dest.writeLong(mEndTimeStamp);
+ dest.writeLong(mLastTimeActive);
+ dest.writeLong(mTotalTimeActive);
+ dest.writeInt(mActivationCount);
+ }
+
+ public static final Creator<ConfigurationStats> CREATOR = new Creator<ConfigurationStats>() {
+ @Override
+ public ConfigurationStats createFromParcel(Parcel source) {
+ ConfigurationStats stats = new ConfigurationStats();
+ if (source.readInt() != 0) {
+ stats.mConfiguration = Configuration.CREATOR.createFromParcel(source);
+ }
+ stats.mBeginTimeStamp = source.readLong();
+ stats.mEndTimeStamp = source.readLong();
+ stats.mLastTimeActive = source.readLong();
+ stats.mTotalTimeActive = source.readLong();
+ stats.mActivationCount = source.readInt();
+ return stats;
+ }
+
+ @Override
+ public ConfigurationStats[] newArray(int size) {
+ return new ConfigurationStats[size];
+ }
+ };
+}
diff --git a/android/app/usage/ExternalStorageStats.java b/android/app/usage/ExternalStorageStats.java
new file mode 100644
index 00000000..f00e5c29
--- /dev/null
+++ b/android/app/usage/ExternalStorageStats.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.BytesLong;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+/**
+ * Shared/external storage statistics for a {@link UserHandle} on a single
+ * storage volume.
+ *
+ * @see StorageStatsManager
+ */
+public final class ExternalStorageStats implements Parcelable {
+ /** {@hide} */ public long totalBytes;
+ /** {@hide} */ public long audioBytes;
+ /** {@hide} */ public long videoBytes;
+ /** {@hide} */ public long imageBytes;
+ /** {@hide} */ public long appBytes;
+ /** {@hide} */ public long obbBytes;
+
+ /**
+ * Return the total bytes used by all files in the shared/external storage
+ * hosted on this volume.
+ * <p>
+ * This value only includes data which is isolated for each user on a
+ * multiuser device. Any OBB data shared between users is not accounted in
+ * this value.
+ */
+ public @BytesLong long getTotalBytes() {
+ return totalBytes;
+ }
+
+ /**
+ * Return the total bytes used by all audio files in the shared/external
+ * storage hosted on this volume.
+ * <p>
+ * This value only includes data which is isolated for each user on a
+ * multiuser device. This value does not include any app files which are all
+ * accounted under {@link #getAppBytes()}.
+ */
+ public @BytesLong long getAudioBytes() {
+ return audioBytes;
+ }
+
+ /**
+ * Return the total bytes used by all video files in the shared/external
+ * storage hosted on this volume.
+ * <p>
+ * This value only includes data which is isolated for each user on a
+ * multiuser device. This value does not include any app files which are all
+ * accounted under {@link #getAppBytes()}.
+ */
+ public @BytesLong long getVideoBytes() {
+ return videoBytes;
+ }
+
+ /**
+ * Return the total bytes used by all image files in the shared/external
+ * storage hosted on this volume.
+ * <p>
+ * This value only includes data which is isolated for each user on a
+ * multiuser device. This value does not include any app files which are all
+ * accounted under {@link #getAppBytes()}.
+ */
+ public @BytesLong long getImageBytes() {
+ return imageBytes;
+ }
+
+ /**
+ * Return the total bytes used by app files in the shared/external storage
+ * hosted on this volume.
+ * <p>
+ * This data is already accounted against individual apps as returned
+ * through {@link StorageStats}.
+ * <p>
+ * This value only includes data which is isolated for each user on a
+ * multiuser device.
+ */
+ public @BytesLong long getAppBytes() {
+ return appBytes;
+ }
+
+ /** {@hide} */
+ public @BytesLong long getObbBytes() {
+ return obbBytes;
+ }
+
+ /** {@hide} */
+ public ExternalStorageStats() {
+ }
+
+ /** {@hide} */
+ public ExternalStorageStats(Parcel in) {
+ this.totalBytes = in.readLong();
+ this.audioBytes = in.readLong();
+ this.videoBytes = in.readLong();
+ this.imageBytes = in.readLong();
+ this.appBytes = in.readLong();
+ this.obbBytes = in.readLong();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(totalBytes);
+ dest.writeLong(audioBytes);
+ dest.writeLong(videoBytes);
+ dest.writeLong(imageBytes);
+ dest.writeLong(appBytes);
+ dest.writeLong(obbBytes);
+ }
+
+ public static final Creator<ExternalStorageStats> CREATOR = new Creator<ExternalStorageStats>() {
+ @Override
+ public ExternalStorageStats createFromParcel(Parcel in) {
+ return new ExternalStorageStats(in);
+ }
+
+ @Override
+ public ExternalStorageStats[] newArray(int size) {
+ return new ExternalStorageStats[size];
+ }
+ };
+}
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
new file mode 100644
index 00000000..222e9a0e
--- /dev/null
+++ b/android/app/usage/NetworkStats.java
@@ -0,0 +1,642 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.IntArray;
+import android.util.Log;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats} objects
+ * are returned as results to various queries in {@link NetworkStatsManager}.
+ */
+public final class NetworkStats implements AutoCloseable {
+ private final static String TAG = "NetworkStats";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Start timestamp of stats collected
+ */
+ private final long mStartTimeStamp;
+
+ /**
+ * End timestamp of stats collected
+ */
+ private final long mEndTimeStamp;
+
+ /**
+ * Non-null array indicates the query enumerates over uids.
+ */
+ private int[] mUids;
+
+ /**
+ * Index of the current uid in mUids when doing uid enumeration or a single uid value,
+ * depending on query type.
+ */
+ private int mUidOrUidIndex;
+
+ /**
+ * Tag id in case if was specified in the query.
+ */
+ private int mTag = android.net.NetworkStats.TAG_NONE;
+
+ /**
+ * The session while the query requires it, null if all the stats have been collected or close()
+ * has been called.
+ */
+ private INetworkStatsSession mSession;
+ private NetworkTemplate mTemplate;
+
+ /**
+ * Results of a summary query.
+ */
+ private android.net.NetworkStats mSummary = null;
+
+ /**
+ * Results of detail queries.
+ */
+ private NetworkStatsHistory mHistory = null;
+
+ /**
+ * Where we are in enumerating over the current result.
+ */
+ private int mEnumerationIndex = 0;
+
+ /**
+ * Recycling entry objects to prevent heap fragmentation.
+ */
+ private android.net.NetworkStats.Entry mRecycledSummaryEntry = null;
+ private NetworkStatsHistory.Entry mRecycledHistoryEntry = null;
+
+ /** @hide */
+ NetworkStats(Context context, NetworkTemplate template, int flags, long startTimestamp,
+ long endTimestamp) throws RemoteException, SecurityException {
+ final INetworkStatsService statsService = INetworkStatsService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+ // Open network stats session
+ mSession = statsService.openSessionForUsageStats(flags, context.getOpPackageName());
+ mCloseGuard.open("close");
+ mTemplate = template;
+ mStartTimeStamp = startTimestamp;
+ mEndTimeStamp = endTimestamp;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ // -------------------------BEGINNING OF PUBLIC API-----------------------------------
+
+ /**
+ * Buckets are the smallest elements of a query result. As some dimensions of a result may be
+ * aggregated (e.g. time or state) some values may be equal across all buckets.
+ */
+ public static class Bucket {
+ /** @hide */
+ @IntDef({STATE_ALL, STATE_DEFAULT, STATE_FOREGROUND})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * Combined usage across all states.
+ */
+ public static final int STATE_ALL = -1;
+
+ /**
+ * Usage not accounted for in any other state.
+ */
+ public static final int STATE_DEFAULT = 0x1;
+
+ /**
+ * Foreground usage.
+ */
+ public static final int STATE_FOREGROUND = 0x2;
+
+ /**
+ * Special UID value for aggregate/unspecified.
+ */
+ public static final int UID_ALL = android.net.NetworkStats.UID_ALL;
+
+ /**
+ * Special UID value for removed apps.
+ */
+ public static final int UID_REMOVED = TrafficStats.UID_REMOVED;
+
+ /**
+ * Special UID value for data usage by tethering.
+ */
+ public static final int UID_TETHERING = TrafficStats.UID_TETHERING;
+
+ /** @hide */
+ @IntDef({METERED_ALL, METERED_NO, METERED_YES})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Metered {}
+
+ /**
+ * Combined usage across all metered states. Covers metered and unmetered usage.
+ */
+ public static final int METERED_ALL = -1;
+
+ /**
+ * Usage that occurs on an unmetered network.
+ */
+ public static final int METERED_NO = 0x1;
+
+ /**
+ * Usage that occurs on a metered network.
+ *
+ * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+ * that connection.
+ */
+ public static final int METERED_YES = 0x2;
+
+ /** @hide */
+ @IntDef({ROAMING_ALL, ROAMING_NO, ROAMING_YES})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Roaming {}
+
+ /**
+ * Combined usage across all roaming states. Covers both roaming and non-roaming usage.
+ */
+ public static final int ROAMING_ALL = -1;
+
+ /**
+ * Usage that occurs on a home, non-roaming network.
+ *
+ * <p>Any cellular usage in this bucket was incurred while the device was connected to a
+ * tower owned or operated by the user's wireless carrier, or a tower that the user's
+ * wireless carrier has indicated should be treated as a home network regardless.
+ *
+ * <p>This is also the default value for network types that do not support roaming.
+ */
+ public static final int ROAMING_NO = 0x1;
+
+ /**
+ * Usage that occurs on a roaming network.
+ *
+ * <p>Any cellular usage in this bucket as incurred while the device was roaming on another
+ * carrier's network, for which additional charges may apply.
+ */
+ public static final int ROAMING_YES = 0x2;
+
+ /**
+ * Special TAG value for total data across all tags
+ */
+ public static final int TAG_NONE = android.net.NetworkStats.TAG_NONE;
+
+ private int mUid;
+ private int mTag;
+ private int mState;
+ private int mMetered;
+ private int mRoaming;
+ private long mBeginTimeStamp;
+ private long mEndTimeStamp;
+ private long mRxBytes;
+ private long mRxPackets;
+ private long mTxBytes;
+ private long mTxPackets;
+
+ private static @State int convertState(int networkStatsSet) {
+ switch (networkStatsSet) {
+ case android.net.NetworkStats.SET_ALL : return STATE_ALL;
+ case android.net.NetworkStats.SET_DEFAULT : return STATE_DEFAULT;
+ case android.net.NetworkStats.SET_FOREGROUND : return STATE_FOREGROUND;
+ }
+ return 0;
+ }
+
+ private static int convertUid(int uid) {
+ switch (uid) {
+ case TrafficStats.UID_REMOVED: return UID_REMOVED;
+ case TrafficStats.UID_TETHERING: return UID_TETHERING;
+ }
+ return uid;
+ }
+
+ private static int convertTag(int tag) {
+ switch (tag) {
+ case android.net.NetworkStats.TAG_NONE: return TAG_NONE;
+ }
+ return tag;
+ }
+
+ private static @Metered int convertMetered(int metered) {
+ switch (metered) {
+ case android.net.NetworkStats.METERED_ALL : return METERED_ALL;
+ case android.net.NetworkStats.METERED_NO: return METERED_NO;
+ case android.net.NetworkStats.METERED_YES: return METERED_YES;
+ }
+ return 0;
+ }
+
+ private static @Roaming int convertRoaming(int roaming) {
+ switch (roaming) {
+ case android.net.NetworkStats.ROAMING_ALL : return ROAMING_ALL;
+ case android.net.NetworkStats.ROAMING_NO: return ROAMING_NO;
+ case android.net.NetworkStats.ROAMING_YES: return ROAMING_YES;
+ }
+ return 0;
+ }
+
+ public Bucket() {
+ }
+
+ /**
+ * Key of the bucket. Usually an app uid or one of the following special values:<p />
+ * <ul>
+ * <li>{@link #UID_REMOVED}</li>
+ * <li>{@link #UID_TETHERING}</li>
+ * <li>{@link android.os.Process#SYSTEM_UID}</li>
+ * </ul>
+ * @return Bucket key.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Tag of the bucket.<p />
+ * @return Bucket tag.
+ */
+ public int getTag() {
+ return mTag;
+ }
+
+ /**
+ * Usage state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #STATE_ALL}</li>
+ * <li>{@link #STATE_DEFAULT}</li>
+ * <li>{@link #STATE_FOREGROUND}</li>
+ * </ul>
+ * @return Usage state.
+ */
+ public @State int getState() {
+ return mState;
+ }
+
+ /**
+ * Metered state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #METERED_ALL}</li>
+ * <li>{@link #METERED_NO}</li>
+ * <li>{@link #METERED_YES}</li>
+ * </ul>
+ * <p>A network is classified as metered when the user is sensitive to heavy data usage on
+ * that connection. Apps may warn before using these networks for large downloads. The
+ * metered state can be set by the user within data usage network restrictions.
+ */
+ public @Metered int getMetered() {
+ return mMetered;
+ }
+
+ /**
+ * Roaming state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #ROAMING_ALL}</li>
+ * <li>{@link #ROAMING_NO}</li>
+ * <li>{@link #ROAMING_YES}</li>
+ * </ul>
+ */
+ public @Roaming int getRoaming() {
+ return mRoaming;
+ }
+
+ /**
+ * Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Start of interval.
+ */
+ public long getStartTimeStamp() {
+ return mBeginTimeStamp;
+ }
+
+ /**
+ * End timestamp of the bucket's time interval. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return End of interval.
+ */
+ public long getEndTimeStamp() {
+ return mEndTimeStamp;
+ }
+
+ /**
+ * Number of bytes received during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of bytes.
+ */
+ public long getRxBytes() {
+ return mRxBytes;
+ }
+
+ /**
+ * Number of bytes transmitted during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of bytes.
+ */
+ public long getTxBytes() {
+ return mTxBytes;
+ }
+
+ /**
+ * Number of packets received during the bucket's time interval. Statistics are measured at
+ * the network layer, so they include both TCP and UDP usage.
+ * @return Number of packets.
+ */
+ public long getRxPackets() {
+ return mRxPackets;
+ }
+
+ /**
+ * Number of packets transmitted during the bucket's time interval. Statistics are measured
+ * at the network layer, so they include both TCP and UDP usage.
+ * @return Number of packets.
+ */
+ public long getTxPackets() {
+ return mTxPackets;
+ }
+ }
+
+ /**
+ * Fills the recycled bucket with data of the next bin in the enumeration.
+ * @param bucketOut Bucket to be filled with data.
+ * @return true if successfully filled the bucket, false otherwise.
+ */
+ public boolean getNextBucket(Bucket bucketOut) {
+ if (mSummary != null) {
+ return getNextSummaryBucket(bucketOut);
+ } else {
+ return getNextHistoryBucket(bucketOut);
+ }
+ }
+
+ /**
+ * Check if it is possible to ask for a next bucket in the enumeration.
+ * @return true if there is at least one more bucket.
+ */
+ public boolean hasNextBucket() {
+ if (mSummary != null) {
+ return mEnumerationIndex < mSummary.size();
+ } else if (mHistory != null) {
+ return mEnumerationIndex < mHistory.size()
+ || hasNextUid();
+ }
+ return false;
+ }
+
+ /**
+ * Closes the enumeration. Call this method before this object gets out of scope.
+ */
+ @Override
+ public void close() {
+ if (mSession != null) {
+ try {
+ mSession.close();
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Otherwise, meh
+ }
+ }
+ mSession = null;
+ if (mCloseGuard != null) {
+ mCloseGuard.close();
+ }
+ }
+
+ // -------------------------END OF PUBLIC API-----------------------------------
+
+ /**
+ * Collects device summary results into a Bucket.
+ * @throws RemoteException
+ */
+ Bucket getDeviceSummaryForNetwork() throws RemoteException {
+ mSummary = mSession.getDeviceSummaryForNetwork(mTemplate, mStartTimeStamp, mEndTimeStamp);
+
+ // Setting enumeration index beyond end to avoid accidental enumeration over data that does
+ // not belong to the calling user.
+ mEnumerationIndex = mSummary.size();
+
+ return getSummaryAggregate();
+ }
+
+ /**
+ * Collects summary results and sets summary enumeration mode.
+ * @throws RemoteException
+ */
+ void startSummaryEnumeration() throws RemoteException {
+ mSummary = mSession.getSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp,
+ false /* includeTags */);
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Collects history results for uid and resets history enumeration index.
+ */
+ void startHistoryEnumeration(int uid) {
+ startHistoryEnumeration(uid, android.net.NetworkStats.TAG_NONE);
+ }
+
+ /**
+ * Collects history results for uid and resets history enumeration index.
+ */
+ void startHistoryEnumeration(int uid, int tag) {
+ mHistory = null;
+ try {
+ mHistory = mSession.getHistoryIntervalForUid(mTemplate, uid,
+ android.net.NetworkStats.SET_ALL, tag,
+ NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ setSingleUidTag(uid, tag);
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Leaving mHistory null
+ }
+ mEnumerationIndex = 0;
+ }
+
+ /**
+ * Starts uid enumeration for current user.
+ * @throws RemoteException
+ */
+ void startUserUidEnumeration() throws RemoteException {
+ // TODO: getRelevantUids should be sensitive to time interval. When that's done,
+ // the filtering logic below can be removed.
+ int[] uids = mSession.getRelevantUids();
+ // Filtering of uids with empty history.
+ IntArray filteredUids = new IntArray(uids.length);
+ for (int uid : uids) {
+ try {
+ NetworkStatsHistory history = mSession.getHistoryIntervalForUid(mTemplate, uid,
+ android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+ NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ if (history != null && history.size() > 0) {
+ filteredUids.add(uid);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error while getting history of uid " + uid, e);
+ }
+ }
+ mUids = filteredUids.toArray();
+ mUidOrUidIndex = -1;
+ stepHistory();
+ }
+
+ /**
+ * Steps to next uid in enumeration and collects history for that.
+ */
+ private void stepHistory(){
+ if (hasNextUid()) {
+ stepUid();
+ mHistory = null;
+ try {
+ mHistory = mSession.getHistoryIntervalForUid(mTemplate, getUid(),
+ android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE,
+ NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp);
+ } catch (RemoteException e) {
+ Log.w(TAG, e);
+ // Leaving mHistory null
+ }
+ mEnumerationIndex = 0;
+ }
+ }
+
+ private void fillBucketFromSummaryEntry(Bucket bucketOut) {
+ bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
+ bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
+ bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
+ bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
+ bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
+ bucketOut.mBeginTimeStamp = mStartTimeStamp;
+ bucketOut.mEndTimeStamp = mEndTimeStamp;
+ bucketOut.mRxBytes = mRecycledSummaryEntry.rxBytes;
+ bucketOut.mRxPackets = mRecycledSummaryEntry.rxPackets;
+ bucketOut.mTxBytes = mRecycledSummaryEntry.txBytes;
+ bucketOut.mTxPackets = mRecycledSummaryEntry.txPackets;
+ }
+
+ /**
+ * Getting the next item in summary enumeration.
+ * @param bucketOut Next item will be set here.
+ * @return true if a next item could be set.
+ */
+ private boolean getNextSummaryBucket(Bucket bucketOut) {
+ if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
+ mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
+ fillBucketFromSummaryEntry(bucketOut);
+ return true;
+ }
+ return false;
+ }
+
+ Bucket getSummaryAggregate() {
+ if (mSummary == null) {
+ return null;
+ }
+ Bucket bucket = new Bucket();
+ if (mRecycledSummaryEntry == null) {
+ mRecycledSummaryEntry = new android.net.NetworkStats.Entry();
+ }
+ mSummary.getTotal(mRecycledSummaryEntry);
+ fillBucketFromSummaryEntry(bucket);
+ return bucket;
+ }
+ /**
+ * Getting the next item in a history enumeration.
+ * @param bucketOut Next item will be set here.
+ * @return true if a next item could be set.
+ */
+ private boolean getNextHistoryBucket(Bucket bucketOut) {
+ if (bucketOut != null && mHistory != null) {
+ if (mEnumerationIndex < mHistory.size()) {
+ mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
+ mRecycledHistoryEntry);
+ bucketOut.mUid = Bucket.convertUid(getUid());
+ bucketOut.mTag = Bucket.convertTag(mTag);
+ bucketOut.mState = Bucket.STATE_ALL;
+ bucketOut.mMetered = Bucket.METERED_ALL;
+ bucketOut.mRoaming = Bucket.ROAMING_ALL;
+ bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
+ bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart +
+ mRecycledHistoryEntry.bucketDuration;
+ bucketOut.mRxBytes = mRecycledHistoryEntry.rxBytes;
+ bucketOut.mRxPackets = mRecycledHistoryEntry.rxPackets;
+ bucketOut.mTxBytes = mRecycledHistoryEntry.txBytes;
+ bucketOut.mTxPackets = mRecycledHistoryEntry.txPackets;
+ return true;
+ } else if (hasNextUid()) {
+ stepHistory();
+ return getNextHistoryBucket(bucketOut);
+ }
+ }
+ return false;
+ }
+
+ // ------------------ UID LOGIC------------------------
+
+ private boolean isUidEnumeration() {
+ return mUids != null;
+ }
+
+ private boolean hasNextUid() {
+ return isUidEnumeration() && (mUidOrUidIndex + 1) < mUids.length;
+ }
+
+ private int getUid() {
+ // Check if uid enumeration.
+ if (isUidEnumeration()) {
+ if (mUidOrUidIndex < 0 || mUidOrUidIndex >= mUids.length) {
+ throw new IndexOutOfBoundsException(
+ "Index=" + mUidOrUidIndex + " mUids.length=" + mUids.length);
+ }
+ return mUids[mUidOrUidIndex];
+ }
+ // Single uid mode.
+ return mUidOrUidIndex;
+ }
+
+ private void setSingleUidTag(int uid, int tag) {
+ mUidOrUidIndex = uid;
+ mTag = tag;
+ }
+
+ private void stepUid() {
+ if (mUids != null) {
+ ++mUidOrUidIndex;
+ }
+ }
+}
diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java
new file mode 100644
index 00000000..853b0033
--- /dev/null
+++ b/android/app/usage/NetworkStatsManager.java
@@ -0,0 +1,494 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.app.usage.NetworkStats.Bucket;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DataUsageRequest;
+import android.net.INetworkStatsService;
+import android.net.NetworkIdentity;
+import android.net.NetworkTemplate;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+
+/**
+ * Provides access to network usage history and statistics. Usage data is collected in
+ * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details.
+ * <p />
+ * Queries can define a time interval in the form of start and end timestamps (Long.MIN_VALUE and
+ * Long.MAX_VALUE can be used to simulate open ended intervals). By default, apps can only obtain
+ * data about themselves. See the below note for special cases in which apps can obtain data about
+ * other applications.
+ * <h3>
+ * Summary queries
+ * </h3>
+ * {@link #querySummaryForDevice} <p />
+ * {@link #querySummaryForUser} <p />
+ * {@link #querySummary} <p />
+ * These queries aggregate network usage across the whole interval. Therefore there will be only one
+ * bucket for a particular key, state, metered and roaming combination. In case of the user-wide
+ * and device-wide summaries a single bucket containing the totalised network usage is returned.
+ * <h3>
+ * History queries
+ * </h3>
+ * {@link #queryDetailsForUid} <p />
+ * {@link #queryDetails} <p />
+ * These queries do not aggregate over time but do aggregate over state, metered and roaming.
+ * Therefore there can be multiple buckets for a particular key but all Bucket's state is going to
+ * be {@link NetworkStats.Bucket#STATE_ALL}, all Bucket's metered is going to be
+ * {@link NetworkStats.Bucket#METERED_ALL}, and all Bucket's roaming is going to be
+ * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p />
+ * <b>NOTE:</b> Calling {@link #querySummaryForDevice} or accessing stats for apps other than the
+ * calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS},
+ * which is a system-level permission and will not be granted to third-party apps. However,
+ * declaring the permission implies intention to use the API and the user of the device can grant
+ * permission through the Settings application.
+ * <p />
+ * Profile owner apps are automatically granted permission to query data on the profile they manage
+ * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps and carrier-
+ * privileged apps likewise get access to usage data for all users on the device.
+ * <p />
+ * In addition to tethering usage, usage by removed users and apps, and usage by the system
+ * is also included in the results for callers with one of these higher levels of access.
+ * <p />
+ * <b>NOTE:</b> Prior to API level {@value android.os.Build.VERSION_CODES#N}, all calls to these APIs required
+ * the above permission, even to access an app's own data usage, and carrier-privileged apps were
+ * not included.
+ */
+@SystemService(Context.NETWORK_STATS_SERVICE)
+public class NetworkStatsManager {
+ private static final String TAG = "NetworkStatsManager";
+ private static final boolean DBG = false;
+
+ /** @hide */
+ public static final int CALLBACK_LIMIT_REACHED = 0;
+ /** @hide */
+ public static final int CALLBACK_RELEASED = 1;
+
+ private final Context mContext;
+ private final INetworkStatsService mService;
+
+ /** @hide */
+ public static final int FLAG_POLL_ON_OPEN = 1 << 0;
+ /** @hide */
+ public static final int FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN = 1 << 1;
+
+ private int mFlags;
+
+ /**
+ * {@hide}
+ */
+ public NetworkStatsManager(Context context) throws ServiceNotFoundException {
+ mContext = context;
+ mService = INetworkStatsService.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.NETWORK_STATS_SERVICE));
+ setPollOnOpen(true);
+ }
+
+ /** @hide */
+ public void setPollOnOpen(boolean pollOnOpen) {
+ if (pollOnOpen) {
+ mFlags |= FLAG_POLL_ON_OPEN;
+ } else {
+ mFlags &= ~FLAG_POLL_ON_OPEN;
+ }
+ }
+
+ /** @hide */
+ public void setAugmentWithSubscriptionPlan(boolean augmentWithSubscriptionPlan) {
+ if (augmentWithSubscriptionPlan) {
+ mFlags |= FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+ } else {
+ mFlags &= ~FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN;
+ }
+ }
+
+ /**
+ * Query network usage statistics summaries. Result is summarised data usage for the whole
+ * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
+ * roaming. This means the bucket's start and end timestamp are going to be the same as the
+ * 'startTime' and 'endTime' parameters. State is going to be
+ * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
+ * tag {@link NetworkStats.Bucket#TAG_NONE}, metered {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Bucket object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ public Bucket querySummaryForDevice(int networkType, String subscriberId,
+ long startTime, long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ Bucket bucket = null;
+ NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ bucket = stats.getDeviceSummaryForNetwork();
+
+ stats.close();
+ return bucket;
+ }
+
+ /**
+ * Query network usage statistics summaries. Result is summarised data usage for all uids
+ * belonging to calling user. Result is a single Bucket aggregated over time, state and uid.
+ * This means the bucket's start and end timestamp are going to be the same as the 'startTime'
+ * and 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL},
+ * uid {@link NetworkStats.Bucket#UID_ALL}, tag {@link NetworkStats.Bucket#TAG_NONE},
+ * metered {@link NetworkStats.Bucket#METERED_ALL}, and roaming
+ * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Bucket object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ public Bucket querySummaryForUser(int networkType, String subscriberId, long startTime,
+ long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ NetworkStats stats;
+ stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ stats.startSummaryEnumeration();
+
+ stats.close();
+ return stats.getSummaryAggregate();
+ }
+
+ /**
+ * Query network usage statistics summaries. Result filtered to include only uids belonging to
+ * calling user. Result is aggregated over time, hence all buckets will have the same start and
+ * end timestamps. Not aggregated over state, uid, metered, or roaming. This means buckets'
+ * start and end timestamps are going to be the same as the 'startTime' and 'endTime'
+ * parameters. State, uid, metered, and roaming are going to vary, and tag is going to be the
+ * same.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ public NetworkStats querySummary(int networkType, String subscriberId, long startTime,
+ long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ NetworkStats result;
+ result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ result.startSummaryEnumeration();
+
+ return result;
+ }
+
+ /**
+ * Query network usage statistics details for a given uid.
+ *
+ * #see queryDetailsForUidTag(int, String, long, long, int, int)
+ */
+ public NetworkStats queryDetailsForUid(int networkType, String subscriberId,
+ long startTime, long endTime, int uid) throws SecurityException, RemoteException {
+ return queryDetailsForUidTag(networkType, subscriberId, startTime, endTime, uid,
+ NetworkStats.Bucket.TAG_NONE);
+ }
+
+ /**
+ * Query network usage statistics details for a given uid and tag. Only usable for uids
+ * belonging to calling user. Result is aggregated over state but not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL}, uid the
+ * same as the 'uid' parameter and tag the same as 'tag' parameter. metered is going to be
+ * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
+ * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for no tags.
+ * @return Statistics object or null if an error happened during statistics collection.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
+ */
+ public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId,
+ long startTime, long endTime, int uid, int tag) throws SecurityException {
+ NetworkTemplate template;
+ template = createTemplate(networkType, subscriberId);
+
+ NetworkStats result;
+ try {
+ result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ result.startHistoryEnumeration(uid, tag);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error while querying stats for uid=" + uid + " tag=" + tag, e);
+ return null;
+ }
+
+ return result;
+ }
+
+ /**
+ * Query network usage statistics details. Result filtered to include only uids belonging to
+ * calling user. Result is aggregated over state but not aggregated over time, uid, tag,
+ * metered, nor roaming. This means buckets' start and end timestamps are going to be between
+ * 'startTime' and 'endTime' parameters. State is going to be
+ * {@link NetworkStats.Bucket#STATE_ALL}, uid will vary,
+ * tag {@link NetworkStats.Bucket#TAG_NONE}, metered is going to be
+ * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
+ * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
+ *
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @return Statistics object or null if permissions are insufficient or error happened during
+ * statistics collection.
+ */
+ public NetworkStats queryDetails(int networkType, String subscriberId, long startTime,
+ long endTime) throws SecurityException, RemoteException {
+ NetworkTemplate template;
+ try {
+ template = createTemplate(networkType, subscriberId);
+ } catch (IllegalArgumentException e) {
+ if (DBG) Log.e(TAG, "Cannot create template", e);
+ return null;
+ }
+
+ NetworkStats result;
+ result = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ result.startUserUidEnumeration();
+ return result;
+ }
+
+ /**
+ * Registers to receive notifications about data usage on specified networks.
+ *
+ * #see registerUsageCallback(int, String[], long, UsageCallback, Handler)
+ */
+ public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
+ UsageCallback callback) {
+ registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
+ null /* handler */);
+ }
+
+ /**
+ * Registers to receive notifications about data usage on specified networks.
+ *
+ * <p>The callbacks will continue to be called as long as the process is live or
+ * {@link #unregisterUsageCallback} is called.
+ *
+ * @param networkType Type of network to monitor. Either
+ {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * @param thresholdBytes Threshold in bytes to be notified on.
+ * @param callback The {@link UsageCallback} that the system will call when data usage
+ * has exceeded the specified threshold.
+ * @param handler to dispatch callback events through, otherwise if {@code null} it uses
+ * the calling thread.
+ */
+ public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
+ UsageCallback callback, @Nullable Handler handler) {
+ checkNotNull(callback, "UsageCallback cannot be null");
+
+ final Looper looper;
+ if (handler == null) {
+ looper = Looper.myLooper();
+ } else {
+ looper = handler.getLooper();
+ }
+
+ if (DBG) {
+ Log.d(TAG, "registerUsageCallback called with: {"
+ + " networkType=" + networkType
+ + " subscriberId=" + subscriberId
+ + " thresholdBytes=" + thresholdBytes
+ + " }");
+ }
+
+ NetworkTemplate template = createTemplate(networkType, subscriberId);
+ DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
+ template, thresholdBytes);
+ try {
+ CallbackHandler callbackHandler = new CallbackHandler(looper, networkType,
+ subscriberId, callback);
+ callback.request = mService.registerUsageCallback(
+ mContext.getOpPackageName(), request, new Messenger(callbackHandler),
+ new Binder());
+ if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
+
+ if (callback.request == null) {
+ Log.e(TAG, "Request from callback is null; should not happen");
+ }
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when registering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregisters callbacks on data usage.
+ *
+ * @param callback The {@link UsageCallback} used when registering.
+ */
+ public void unregisterUsageCallback(UsageCallback callback) {
+ if (callback == null || callback.request == null
+ || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
+ throw new IllegalArgumentException("Invalid UsageCallback");
+ }
+ try {
+ mService.unregisterUsageRequest(callback.request);
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when unregistering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Base class for usage callbacks. Should be extended by applications wanting notifications.
+ */
+ public static abstract class UsageCallback {
+
+ /**
+ * Called when data usage has reached the given threshold.
+ */
+ public abstract void onThresholdReached(int networkType, String subscriberId);
+
+ /**
+ * @hide used for internal bookkeeping
+ */
+ private DataUsageRequest request;
+ }
+
+ private static NetworkTemplate createTemplate(int networkType, String subscriberId) {
+ NetworkTemplate template = null;
+ switch (networkType) {
+ case ConnectivityManager.TYPE_MOBILE: {
+ template = NetworkTemplate.buildTemplateMobileAll(subscriberId);
+ } break;
+ case ConnectivityManager.TYPE_WIFI: {
+ template = NetworkTemplate.buildTemplateWifiWildcard();
+ } break;
+ default: {
+ throw new IllegalArgumentException("Cannot create template for network type "
+ + networkType + ", subscriberId '"
+ + NetworkIdentity.scrubSubscriberId(subscriberId) + "'.");
+ }
+ }
+ return template;
+ }
+
+ private static class CallbackHandler extends Handler {
+ private final int mNetworkType;
+ private final String mSubscriberId;
+ private UsageCallback mCallback;
+
+ CallbackHandler(Looper looper, int networkType, String subscriberId,
+ UsageCallback callback) {
+ super(looper);
+ mNetworkType = networkType;
+ mSubscriberId = subscriberId;
+ mCallback = callback;
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ DataUsageRequest request =
+ (DataUsageRequest) getObject(message, DataUsageRequest.PARCELABLE_KEY);
+
+ switch (message.what) {
+ case CALLBACK_LIMIT_REACHED: {
+ if (mCallback != null) {
+ mCallback.onThresholdReached(mNetworkType, mSubscriberId);
+ } else {
+ Log.e(TAG, "limit reached with released callback for " + request);
+ }
+ break;
+ }
+ case CALLBACK_RELEASED: {
+ if (DBG) Log.d(TAG, "callback released for " + request);
+ mCallback = null;
+ break;
+ }
+ }
+ }
+
+ private static Object getObject(Message msg, String key) {
+ return msg.getData().getParcelable(key);
+ }
+ }
+}
diff --git a/android/app/usage/StorageStats.java b/android/app/usage/StorageStats.java
new file mode 100644
index 00000000..3a277518
--- /dev/null
+++ b/android/app/usage/StorageStats.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.BytesLong;
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+/**
+ * Storage statistics for a UID, package, or {@link UserHandle} on a single
+ * storage volume.
+ *
+ * @see StorageStatsManager
+ */
+public final class StorageStats implements Parcelable {
+ /** {@hide} */ public long codeBytes;
+ /** {@hide} */ public long dataBytes;
+ /** {@hide} */ public long cacheBytes;
+
+ /**
+ * Return the size of app. This includes {@code APK} files, optimized
+ * compiler output, and unpacked native libraries.
+ * <p>
+ * If the primary external/shared storage is hosted on this storage device,
+ * then this includes files stored under {@link Context#getObbDir()}.
+ * <p>
+ * Code is shared between all users on a multiuser device.
+ */
+ public @BytesLong long getAppBytes() {
+ return codeBytes;
+ }
+
+ /** @removed */
+ @Deprecated
+ public long getCodeBytes() {
+ return getAppBytes();
+ }
+
+ /**
+ * Return the size of all data. This includes files stored under
+ * {@link Context#getDataDir()}, {@link Context#getCacheDir()},
+ * {@link Context#getCodeCacheDir()}.
+ * <p>
+ * If the primary external/shared storage is hosted on this storage device,
+ * then this includes files stored under
+ * {@link Context#getExternalFilesDir(String)},
+ * {@link Context#getExternalCacheDir()}, and
+ * {@link Context#getExternalMediaDirs()}.
+ * <p>
+ * Data is isolated for each user on a multiuser device.
+ */
+ public @BytesLong long getDataBytes() {
+ return dataBytes;
+ }
+
+ /**
+ * Return the size of all cached data. This includes files stored under
+ * {@link Context#getCacheDir()} and {@link Context#getCodeCacheDir()}.
+ * <p>
+ * If the primary external/shared storage is hosted on this storage device,
+ * then this includes files stored under
+ * {@link Context#getExternalCacheDir()}.
+ * <p>
+ * Cached data is isolated for each user on a multiuser device.
+ */
+ public @BytesLong long getCacheBytes() {
+ return cacheBytes;
+ }
+
+ /** {@hide} */
+ public StorageStats() {
+ }
+
+ /** {@hide} */
+ public StorageStats(Parcel in) {
+ this.codeBytes = in.readLong();
+ this.dataBytes = in.readLong();
+ this.cacheBytes = in.readLong();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(codeBytes);
+ dest.writeLong(dataBytes);
+ dest.writeLong(cacheBytes);
+ }
+
+ public static final Creator<StorageStats> CREATOR = new Creator<StorageStats>() {
+ @Override
+ public StorageStats createFromParcel(Parcel in) {
+ return new StorageStats(in);
+ }
+
+ @Override
+ public StorageStats[] newArray(int size) {
+ return new StorageStats[size];
+ }
+ };
+}
diff --git a/android/app/usage/StorageStatsManager.java b/android/app/usage/StorageStatsManager.java
new file mode 100644
index 00000000..3d187ec7
--- /dev/null
+++ b/android/app/usage/StorageStatsManager.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.usage;
+
+import static android.os.storage.StorageManager.convert;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.ParcelableException;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.storage.StorageManager;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * Access to detailed storage statistics. This provides a summary of how apps,
+ * users, and external/shared storage is utilizing disk space.
+ * <p class="note">
+ * Note: no permissions are required when calling these APIs for your own
+ * package or UID. However, requesting details for any other package requires
+ * the {@code android.Manifest.permission#PACKAGE_USAGE_STATS} permission, which
+ * is a system-level permission that will not be granted to normal apps.
+ * Declaring that permission expresses your intention to use this API and an end
+ * user can then choose to grant this permission through the Settings
+ * application.
+ * </p>
+ */
+@SystemService(Context.STORAGE_STATS_SERVICE)
+public class StorageStatsManager {
+ private final Context mContext;
+ private final IStorageStatsManager mService;
+
+ /** {@hide} */
+ public StorageStatsManager(Context context, IStorageStatsManager service) {
+ mContext = Preconditions.checkNotNull(context);
+ mService = Preconditions.checkNotNull(service);
+ }
+
+ /** {@hide} */
+ @TestApi
+ public boolean isQuotaSupported(@NonNull UUID storageUuid) {
+ try {
+ return mService.isQuotaSupported(convert(storageUuid), mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public boolean isQuotaSupported(String uuid) {
+ return isQuotaSupported(convert(uuid));
+ }
+
+ /**
+ * Return the total size of the underlying physical media that is hosting
+ * this storage volume.
+ * <p>
+ * This value is best suited for visual display to end users, since it's
+ * designed to reflect the total storage size advertised in a retail
+ * environment.
+ * <p>
+ * Apps making logical decisions about disk space should always use
+ * {@link File#getTotalSpace()} instead of this value.
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @throws IOException when the storage device isn't present.
+ */
+ @WorkerThread
+ public @BytesLong long getTotalBytes(@NonNull UUID storageUuid) throws IOException {
+ try {
+ return mService.getTotalBytes(convert(storageUuid), mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public long getTotalBytes(String uuid) throws IOException {
+ return getTotalBytes(convert(uuid));
+ }
+
+ /**
+ * Return the free space on the requested storage volume.
+ * <p>
+ * This value is best suited for visual display to end users, since it's
+ * designed to reflect both unused space <em>and</em> and cached space that
+ * could be reclaimed by the system.
+ * <p>
+ * Apps making logical decisions about disk space should always use
+ * {@link StorageManager#getAllocatableBytes(UUID)} instead of this value.
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @throws IOException when the storage device isn't present.
+ */
+ @WorkerThread
+ public @BytesLong long getFreeBytes(@NonNull UUID storageUuid) throws IOException {
+ try {
+ return mService.getFreeBytes(convert(storageUuid), mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public long getFreeBytes(String uuid) throws IOException {
+ return getFreeBytes(convert(uuid));
+ }
+
+ /** {@hide} */
+ public @BytesLong long getCacheBytes(@NonNull UUID storageUuid) throws IOException {
+ try {
+ return mService.getCacheBytes(convert(storageUuid), mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ @Deprecated
+ public long getCacheBytes(String uuid) throws IOException {
+ return getCacheBytes(convert(uuid));
+ }
+
+ /**
+ * Return storage statistics for a specific package on the requested storage
+ * volume.
+ * <p class="note">
+ * Note: no permissions are required when calling this API for your own
+ * package. However, requesting details for any other package requires the
+ * {@code android.Manifest.permission#PACKAGE_USAGE_STATS} permission, which
+ * is a system-level permission that will not be granted to normal apps.
+ * Declaring that permission expresses your intention to use this API and an
+ * end user can then choose to grant this permission through the Settings
+ * application.
+ * </p>
+ * <p class="note">
+ * Note: if the requested package uses the {@code android:sharedUserId}
+ * manifest feature, this call will be forced into a slower manual
+ * calculation path. If possible, consider always using
+ * {@link #queryStatsForUid(UUID, int)}, which is typically faster.
+ * </p>
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @param packageName the package name you're interested in.
+ * @param user the user you're interested in.
+ * @throws PackageManager.NameNotFoundException when the requested package
+ * name isn't installed for the requested user.
+ * @throws IOException when the storage device isn't present.
+ * @see ApplicationInfo#storageUuid
+ * @see PackageInfo#packageName
+ */
+ @WorkerThread
+ public @NonNull StorageStats queryStatsForPackage(@NonNull UUID storageUuid,
+ @NonNull String packageName, @NonNull UserHandle user)
+ throws PackageManager.NameNotFoundException, IOException {
+ try {
+ return mService.queryStatsForPackage(convert(storageUuid), packageName,
+ user.getIdentifier(), mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public StorageStats queryStatsForPackage(String uuid, String packageName,
+ UserHandle user) throws PackageManager.NameNotFoundException, IOException {
+ return queryStatsForPackage(convert(uuid), packageName, user);
+ }
+
+ /**
+ * Return storage statistics for a specific UID on the requested storage
+ * volume.
+ * <p class="note">
+ * Note: no permissions are required when calling this API for your own UID.
+ * However, requesting details for any other UID requires the
+ * {@code android.Manifest.permission#PACKAGE_USAGE_STATS} permission, which
+ * is a system-level permission that will not be granted to normal apps.
+ * Declaring that permission expresses your intention to use this API and an
+ * end user can then choose to grant this permission through the Settings
+ * application.
+ * </p>
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @param uid the UID you're interested in.
+ * @throws IOException when the storage device isn't present.
+ * @see ApplicationInfo#storageUuid
+ * @see ApplicationInfo#uid
+ */
+ @WorkerThread
+ public @NonNull StorageStats queryStatsForUid(@NonNull UUID storageUuid, int uid)
+ throws IOException {
+ try {
+ return mService.queryStatsForUid(convert(storageUuid), uid,
+ mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public StorageStats queryStatsForUid(String uuid, int uid) throws IOException {
+ return queryStatsForUid(convert(uuid), uid);
+ }
+
+ /**
+ * Return storage statistics for a specific {@link UserHandle} on the
+ * requested storage volume.
+ * <p class="note">
+ * Note: this API requires the
+ * {@code android.Manifest.permission#PACKAGE_USAGE_STATS} permission, which
+ * is a system-level permission that will not be granted to normal apps.
+ * Declaring that permission expresses your intention to use this API and an
+ * end user can then choose to grant this permission through the Settings
+ * application.
+ * </p>
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @param user the user you're interested in.
+ * @throws IOException when the storage device isn't present.
+ * @see android.os.Process#myUserHandle()
+ */
+ @WorkerThread
+ public @NonNull StorageStats queryStatsForUser(@NonNull UUID storageUuid,
+ @NonNull UserHandle user) throws IOException {
+ try {
+ return mService.queryStatsForUser(convert(storageUuid), user.getIdentifier(),
+ mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public StorageStats queryStatsForUser(String uuid, UserHandle user) throws IOException {
+ return queryStatsForUser(convert(uuid), user);
+ }
+
+ /**
+ * Return shared/external storage statistics for a specific
+ * {@link UserHandle} on the requested storage volume.
+ * <p class="note">
+ * Note: this API requires the
+ * {@code android.Manifest.permission#PACKAGE_USAGE_STATS} permission, which
+ * is a system-level permission that will not be granted to normal apps.
+ * Declaring that permission expresses your intention to use this API and an
+ * end user can then choose to grant this permission through the Settings
+ * application.
+ * </p>
+ *
+ * @param storageUuid the UUID of the storage volume you're interested in,
+ * such as {@link StorageManager#UUID_DEFAULT}.
+ * @throws IOException when the storage device isn't present.
+ * @see android.os.Process#myUserHandle()
+ */
+ @WorkerThread
+ public @NonNull ExternalStorageStats queryExternalStatsForUser(@NonNull UUID storageUuid,
+ @NonNull UserHandle user) throws IOException {
+ try {
+ return mService.queryExternalStatsForUser(convert(storageUuid), user.getIdentifier(),
+ mContext.getOpPackageName());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(IOException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @removed */
+ @Deprecated
+ public ExternalStorageStats queryExternalStatsForUser(String uuid, UserHandle user)
+ throws IOException {
+ return queryExternalStatsForUser(convert(uuid), user);
+ }
+
+ /** {@hide} */
+ public long getCacheQuotaBytes(String volumeUuid, int uid) {
+ try {
+ return mService.getCacheQuotaBytes(volumeUuid, uid, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/app/usage/TimeSparseArray.java b/android/app/usage/TimeSparseArray.java
new file mode 100644
index 00000000..7974fa70
--- /dev/null
+++ b/android/app/usage/TimeSparseArray.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.util.LongSparseArray;
+
+/**
+ * An array that indexes by a long timestamp, representing milliseconds since the epoch.
+ *
+ * {@hide}
+ */
+public class TimeSparseArray<E> extends LongSparseArray<E> {
+ public TimeSparseArray() {
+ super();
+ }
+
+ public TimeSparseArray(int initialCapacity) {
+ super(initialCapacity);
+ }
+
+ /**
+ * Finds the index of the first element whose timestamp is greater or equal to
+ * the given time.
+ *
+ * @param time The timestamp for which to search the array.
+ * @return The index of the matched element, or -1 if no such match exists.
+ */
+ public int closestIndexOnOrAfter(long time) {
+ // This is essentially a binary search, except that if no match is found
+ // the closest index is returned.
+ final int size = size();
+ int lo = 0;
+ int hi = size - 1;
+ int mid = -1;
+ long key = -1;
+ while (lo <= hi) {
+ mid = lo + ((hi - lo) / 2);
+ key = keyAt(mid);
+
+ if (time > key) {
+ lo = mid + 1;
+ } else if (time < key) {
+ hi = mid - 1;
+ } else {
+ return mid;
+ }
+ }
+
+ if (time < key) {
+ return mid;
+ } else if (time > key && lo < size) {
+ return lo;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Finds the index of the first element whose timestamp is less than or equal to
+ * the given time.
+ *
+ * @param time The timestamp for which to search the array.
+ * @return The index of the matched element, or -1 if no such match exists.
+ */
+ public int closestIndexOnOrBefore(long time) {
+ final int index = closestIndexOnOrAfter(time);
+ if (index < 0) {
+ // Everything is larger, so we use the last element, or -1 if the list is empty.
+ return size() - 1;
+ }
+
+ if (keyAt(index) == time) {
+ return index;
+ }
+ return index - 1;
+ }
+}
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
new file mode 100644
index 00000000..0d7a9413
--- /dev/null
+++ b/android/app/usage/UsageEvents.java
@@ -0,0 +1,506 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package android.app.usage;
+
+import android.annotation.IntDef;
+import android.content.res.Configuration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A result returned from {@link android.app.usage.UsageStatsManager#queryEvents(long, long)}
+ * from which to read {@link android.app.usage.UsageEvents.Event} objects.
+ */
+public final class UsageEvents implements Parcelable {
+
+ /** @hide */
+ public static final String INSTANT_APP_PACKAGE_NAME = "android.instant_app";
+
+ /** @hide */
+ public static final String INSTANT_APP_CLASS_NAME = "android.instant_class";
+
+ /**
+ * An event representing a state change for a component.
+ */
+ public static final class Event {
+
+ /**
+ * No event type.
+ */
+ public static final int NONE = 0;
+
+ /**
+ * An event type denoting that a component moved to the foreground.
+ */
+ public static final int MOVE_TO_FOREGROUND = 1;
+
+ /**
+ * An event type denoting that a component moved to the background.
+ */
+ public static final int MOVE_TO_BACKGROUND = 2;
+
+ /**
+ * An event type denoting that a component was in the foreground when the stats
+ * rolled-over. This is effectively treated as a {@link #MOVE_TO_BACKGROUND}.
+ * {@hide}
+ */
+ public static final int END_OF_DAY = 3;
+
+ /**
+ * An event type denoting that a component was in the foreground the previous day.
+ * This is effectively treated as a {@link #MOVE_TO_FOREGROUND}.
+ * {@hide}
+ */
+ public static final int CONTINUE_PREVIOUS_DAY = 4;
+
+ /**
+ * An event type denoting that the device configuration has changed.
+ */
+ public static final int CONFIGURATION_CHANGE = 5;
+
+ /**
+ * An event type denoting that a package was interacted with in some way by the system.
+ * @hide
+ */
+ public static final int SYSTEM_INTERACTION = 6;
+
+ /**
+ * An event type denoting that a package was interacted with in some way by the user.
+ */
+ public static final int USER_INTERACTION = 7;
+
+ /**
+ * An event type denoting that an action equivalent to a ShortcutInfo is taken by the user.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public static final int SHORTCUT_INVOCATION = 8;
+
+ /**
+ * An event type denoting that a package was selected by the user for ChooserActivity.
+ * @hide
+ */
+ public static final int CHOOSER_ACTION = 9;
+
+ /** @hide */
+ public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ FLAG_IS_PACKAGE_INSTANT_APP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventFlags {}
+
+ /**
+ * {@hide}
+ */
+ public String mPackage;
+
+ /**
+ * {@hide}
+ */
+ public String mClass;
+
+ /**
+ * {@hide}
+ */
+ public long mTimeStamp;
+
+ /**
+ * {@hide}
+ */
+ public int mEventType;
+
+ /**
+ * Only present for {@link #CONFIGURATION_CHANGE} event types.
+ * {@hide}
+ */
+ public Configuration mConfiguration;
+
+ /**
+ * ID of the shortcut.
+ * Only present for {@link #SHORTCUT_INVOCATION} event types.
+ * {@hide}
+ */
+ public String mShortcutId;
+
+ /**
+ * Action type passed to ChooserActivity
+ * Only present for {@link #CHOOSER_ACTION} event types.
+ * {@hide}
+ */
+ public String mAction;
+
+ /**
+ * Content type passed to ChooserActivity.
+ * Only present for {@link #CHOOSER_ACTION} event types.
+ * {@hide}
+ */
+ public String mContentType;
+
+ /**
+ * Content annotations passed to ChooserActivity.
+ * Only present for {@link #CHOOSER_ACTION} event types.
+ * {@hide}
+ */
+ public String[] mContentAnnotations;
+
+ /** @hide */
+ @EventFlags
+ public int mFlags;
+
+ public Event() {
+ }
+
+ /** @hide */
+ public Event(Event orig) {
+ mPackage = orig.mPackage;
+ mClass = orig.mClass;
+ mTimeStamp = orig.mTimeStamp;
+ mEventType = orig.mEventType;
+ mConfiguration = orig.mConfiguration;
+ mShortcutId = orig.mShortcutId;
+ mAction = orig.mAction;
+ mContentType = orig.mContentType;
+ mContentAnnotations = orig.mContentAnnotations;
+ mFlags = orig.mFlags;
+ }
+
+ /**
+ * The package name of the source of this event.
+ */
+ public String getPackageName() {
+ return mPackage;
+ }
+
+ /**
+ * The class name of the source of this event. This may be null for
+ * certain events.
+ */
+ public String getClassName() {
+ return mClass;
+ }
+
+ /**
+ * The time at which this event occurred, measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getTimeStamp() {
+ return mTimeStamp;
+ }
+
+ /**
+ * The event type.
+ *
+ * See {@link #MOVE_TO_BACKGROUND}
+ * See {@link #MOVE_TO_FOREGROUND}
+ */
+ public int getEventType() {
+ return mEventType;
+ }
+
+ /**
+ * Returns a {@link Configuration} for this event if the event is of type
+ * {@link #CONFIGURATION_CHANGE}, otherwise it returns null.
+ */
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Returns the ID of a {@link android.content.pm.ShortcutInfo} for this event
+ * if the event is of type {@link #SHORTCUT_INVOCATION}, otherwise it returns null.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public String getShortcutId() {
+ return mShortcutId;
+ }
+
+ /** @hide */
+ public Event getObfuscatedIfInstantApp() {
+ if ((mFlags & FLAG_IS_PACKAGE_INSTANT_APP) == 0) {
+ return this;
+ }
+ final Event ret = new Event(this);
+ ret.mPackage = INSTANT_APP_PACKAGE_NAME;
+ ret.mClass = INSTANT_APP_CLASS_NAME;
+
+ // Note there are other string fields too, but they're for app shortcuts and choosers,
+ // which instant apps can't use anyway, so there's no need to hide them.
+ return ret;
+ }
+ }
+
+ // Only used when creating the resulting events. Not used for reading/unparceling.
+ private List<Event> mEventsToWrite = null;
+
+ // Only used for reading/unparceling events.
+ private Parcel mParcel = null;
+ private final int mEventCount;
+
+ private int mIndex = 0;
+
+ /*
+ * In order to save space, since ComponentNames will be duplicated everywhere,
+ * we use a map and index into it.
+ */
+ private String[] mStringPool;
+
+ /**
+ * Construct the iterator from a parcel.
+ * {@hide}
+ */
+ public UsageEvents(Parcel in) {
+ mEventCount = in.readInt();
+ mIndex = in.readInt();
+ if (mEventCount > 0) {
+ mStringPool = in.createStringArray();
+
+ final int listByteLength = in.readInt();
+ final int positionInParcel = in.readInt();
+ mParcel = Parcel.obtain();
+ mParcel.setDataPosition(0);
+ mParcel.appendFrom(in, in.dataPosition(), listByteLength);
+ mParcel.setDataSize(mParcel.dataPosition());
+ mParcel.setDataPosition(positionInParcel);
+ }
+ }
+
+ /**
+ * Create an empty iterator.
+ * {@hide}
+ */
+ UsageEvents() {
+ mEventCount = 0;
+ }
+
+ /**
+ * Construct the iterator in preparation for writing it to a parcel.
+ * {@hide}
+ */
+ public UsageEvents(List<Event> events, String[] stringPool) {
+ mStringPool = stringPool;
+ mEventCount = events.size();
+ mEventsToWrite = events;
+ }
+
+ /**
+ * Returns whether or not there are more events to read using
+ * {@link #getNextEvent(android.app.usage.UsageEvents.Event)}.
+ *
+ * @return true if there are more events, false otherwise.
+ */
+ public boolean hasNextEvent() {
+ return mIndex < mEventCount;
+ }
+
+ /**
+ * Retrieve the next {@link android.app.usage.UsageEvents.Event} from the collection and put the
+ * resulting data into {@code eventOut}.
+ *
+ * @param eventOut The {@link android.app.usage.UsageEvents.Event} object that will receive the
+ * next event data.
+ * @return true if an event was available, false if there are no more events.
+ */
+ public boolean getNextEvent(Event eventOut) {
+ if (mIndex >= mEventCount) {
+ return false;
+ }
+
+ readEventFromParcel(mParcel, eventOut);
+
+ mIndex++;
+ if (mIndex >= mEventCount) {
+ mParcel.recycle();
+ mParcel = null;
+ }
+ return true;
+ }
+
+ /**
+ * Resets the collection so that it can be iterated over from the beginning.
+ *
+ * @hide When this object is iterated to completion, the parcel is destroyed and
+ * so resetToStart doesn't work.
+ */
+ public void resetToStart() {
+ mIndex = 0;
+ if (mParcel != null) {
+ mParcel.setDataPosition(0);
+ }
+ }
+
+ private int findStringIndex(String str) {
+ final int index = Arrays.binarySearch(mStringPool, str);
+ if (index < 0) {
+ throw new IllegalStateException("String '" + str + "' is not in the string pool");
+ }
+ return index;
+ }
+
+ /**
+ * Writes a single event to the parcel. Modify this when updating {@link Event}.
+ */
+ private void writeEventToParcel(Event event, Parcel p, int flags) {
+ final int packageIndex;
+ if (event.mPackage != null) {
+ packageIndex = findStringIndex(event.mPackage);
+ } else {
+ packageIndex = -1;
+ }
+
+ final int classIndex;
+ if (event.mClass != null) {
+ classIndex = findStringIndex(event.mClass);
+ } else {
+ classIndex = -1;
+ }
+ p.writeInt(packageIndex);
+ p.writeInt(classIndex);
+ p.writeInt(event.mEventType);
+ p.writeLong(event.mTimeStamp);
+
+ switch (event.mEventType) {
+ case Event.CONFIGURATION_CHANGE:
+ event.mConfiguration.writeToParcel(p, flags);
+ break;
+ case Event.SHORTCUT_INVOCATION:
+ p.writeString(event.mShortcutId);
+ break;
+ case Event.CHOOSER_ACTION:
+ p.writeString(event.mAction);
+ p.writeString(event.mContentType);
+ p.writeStringArray(event.mContentAnnotations);
+ break;
+ }
+ }
+
+ /**
+ * Reads a single event from the parcel. Modify this when updating {@link Event}.
+ */
+ private void readEventFromParcel(Parcel p, Event eventOut) {
+ final int packageIndex = p.readInt();
+ if (packageIndex >= 0) {
+ eventOut.mPackage = mStringPool[packageIndex];
+ } else {
+ eventOut.mPackage = null;
+ }
+
+ final int classIndex = p.readInt();
+ if (classIndex >= 0) {
+ eventOut.mClass = mStringPool[classIndex];
+ } else {
+ eventOut.mClass = null;
+ }
+ eventOut.mEventType = p.readInt();
+ eventOut.mTimeStamp = p.readLong();
+
+ // Fill out the event-dependant fields.
+ eventOut.mConfiguration = null;
+ eventOut.mShortcutId = null;
+ eventOut.mAction = null;
+ eventOut.mContentType = null;
+ eventOut.mContentAnnotations = null;
+
+ switch (eventOut.mEventType) {
+ case Event.CONFIGURATION_CHANGE:
+ // Extract the configuration for configuration change events.
+ eventOut.mConfiguration = Configuration.CREATOR.createFromParcel(p);
+ break;
+ case Event.SHORTCUT_INVOCATION:
+ eventOut.mShortcutId = p.readString();
+ break;
+ case Event.CHOOSER_ACTION:
+ eventOut.mAction = p.readString();
+ eventOut.mContentType = p.readString();
+ eventOut.mContentAnnotations = p.createStringArray();
+ break;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEventCount);
+ dest.writeInt(mIndex);
+ if (mEventCount > 0) {
+ dest.writeStringArray(mStringPool);
+
+ if (mEventsToWrite != null) {
+ // Write out the events
+ Parcel p = Parcel.obtain();
+ try {
+ p.setDataPosition(0);
+ for (int i = 0; i < mEventCount; i++) {
+ final Event event = mEventsToWrite.get(i);
+ writeEventToParcel(event, p, flags);
+ }
+
+ final int listByteLength = p.dataPosition();
+
+ // Write the total length of the data.
+ dest.writeInt(listByteLength);
+
+ // Write our current position into the data.
+ dest.writeInt(0);
+
+ // Write the data.
+ dest.appendFrom(p, 0, listByteLength);
+ } finally {
+ p.recycle();
+ }
+
+ } else if (mParcel != null) {
+ // Write the total length of the data.
+ dest.writeInt(mParcel.dataSize());
+
+ // Write out current position into the data.
+ dest.writeInt(mParcel.dataPosition());
+
+ // Write the data.
+ dest.appendFrom(mParcel, 0, mParcel.dataSize());
+ } else {
+ throw new IllegalStateException(
+ "Either mParcel or mEventsToWrite must not be null");
+ }
+ }
+ }
+
+ public static final Creator<UsageEvents> CREATOR = new Creator<UsageEvents>() {
+ @Override
+ public UsageEvents createFromParcel(Parcel source) {
+ return new UsageEvents(source);
+ }
+
+ @Override
+ public UsageEvents[] newArray(int size) {
+ return new UsageEvents[size];
+ }
+ };
+}
diff --git a/android/app/usage/UsageStats.java b/android/app/usage/UsageStats.java
new file mode 100644
index 00000000..7eef85c4
--- /dev/null
+++ b/android/app/usage/UsageStats.java
@@ -0,0 +1,255 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+/**
+ * Contains usage statistics for an app package for a specific
+ * time range.
+ */
+public final class UsageStats implements Parcelable {
+
+ /**
+ * {@hide}
+ */
+ public String mPackageName;
+
+ /**
+ * {@hide}
+ */
+ public long mBeginTimeStamp;
+
+ /**
+ * {@hide}
+ */
+ public long mEndTimeStamp;
+
+ /**
+ * Last time used by the user with an explicit action (notification, activity launch).
+ * {@hide}
+ */
+ public long mLastTimeUsed;
+
+ /**
+ * {@hide}
+ */
+ public long mTotalTimeInForeground;
+
+ /**
+ * {@hide}
+ */
+ public int mLaunchCount;
+
+ /**
+ * {@hide}
+ */
+ public int mLastEvent;
+
+ /**
+ * {@hide}
+ */
+ public ArrayMap<String, ArrayMap<String, Integer>> mChooserCounts;
+
+ /**
+ * {@hide}
+ */
+ public UsageStats() {
+ }
+
+ public UsageStats(UsageStats stats) {
+ mPackageName = stats.mPackageName;
+ mBeginTimeStamp = stats.mBeginTimeStamp;
+ mEndTimeStamp = stats.mEndTimeStamp;
+ mLastTimeUsed = stats.mLastTimeUsed;
+ mTotalTimeInForeground = stats.mTotalTimeInForeground;
+ mLaunchCount = stats.mLaunchCount;
+ mLastEvent = stats.mLastEvent;
+ mChooserCounts = stats.mChooserCounts;
+ }
+
+ /**
+ * {@hide}
+ */
+ public UsageStats getObfuscatedForInstantApp() {
+ final UsageStats ret = new UsageStats(this);
+
+ ret.mPackageName = UsageEvents.INSTANT_APP_PACKAGE_NAME;
+
+ return ret;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Get the beginning of the time range this {@link android.app.usage.UsageStats} represents,
+ * measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getFirstTimeStamp() {
+ return mBeginTimeStamp;
+ }
+
+ /**
+ * Get the end of the time range this {@link android.app.usage.UsageStats} represents,
+ * measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getLastTimeStamp() {
+ return mEndTimeStamp;
+ }
+
+ /**
+ * Get the last time this package was used, measured in milliseconds since the epoch.
+ * <p/>
+ * See {@link System#currentTimeMillis()}.
+ */
+ public long getLastTimeUsed() {
+ return mLastTimeUsed;
+ }
+
+ /**
+ * Get the total time this package spent in the foreground, measured in milliseconds.
+ */
+ public long getTotalTimeInForeground() {
+ return mTotalTimeInForeground;
+ }
+
+ /**
+ * Add the statistics from the right {@link UsageStats} to the left. The package name for
+ * both {@link UsageStats} objects must be the same.
+ * @param right The {@link UsageStats} object to merge into this one.
+ * @throws java.lang.IllegalArgumentException if the package names of the two
+ * {@link UsageStats} objects are different.
+ */
+ public void add(UsageStats right) {
+ if (!mPackageName.equals(right.mPackageName)) {
+ throw new IllegalArgumentException("Can't merge UsageStats for package '" +
+ mPackageName + "' with UsageStats for package '" + right.mPackageName + "'.");
+ }
+
+ // We use the mBeginTimeStamp due to a bug where UsageStats files can overlap with
+ // regards to their mEndTimeStamp.
+ if (right.mBeginTimeStamp > mBeginTimeStamp) {
+ // Even though incoming UsageStat begins after this one, its last time used fields
+ // may somehow be empty or chronologically preceding the older UsageStat.
+ mLastEvent = Math.max(mLastEvent, right.mLastEvent);
+ mLastTimeUsed = Math.max(mLastTimeUsed, right.mLastTimeUsed);
+ }
+ mBeginTimeStamp = Math.min(mBeginTimeStamp, right.mBeginTimeStamp);
+ mEndTimeStamp = Math.max(mEndTimeStamp, right.mEndTimeStamp);
+ mTotalTimeInForeground += right.mTotalTimeInForeground;
+ mLaunchCount += right.mLaunchCount;
+ if (mChooserCounts == null) {
+ mChooserCounts = right.mChooserCounts;
+ } else if (right.mChooserCounts != null) {
+ final int chooserCountsSize = right.mChooserCounts.size();
+ for (int i = 0; i < chooserCountsSize; i++) {
+ String action = right.mChooserCounts.keyAt(i);
+ ArrayMap<String, Integer> counts = right.mChooserCounts.valueAt(i);
+ if (!mChooserCounts.containsKey(action) || mChooserCounts.get(action) == null) {
+ mChooserCounts.put(action, counts);
+ continue;
+ }
+ final int annotationSize = counts.size();
+ for (int j = 0; j < annotationSize; j++) {
+ String key = counts.keyAt(j);
+ int rightValue = counts.valueAt(j);
+ int leftValue = mChooserCounts.get(action).getOrDefault(key, 0);
+ mChooserCounts.get(action).put(key, leftValue + rightValue);
+ }
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPackageName);
+ dest.writeLong(mBeginTimeStamp);
+ dest.writeLong(mEndTimeStamp);
+ dest.writeLong(mLastTimeUsed);
+ dest.writeLong(mTotalTimeInForeground);
+ dest.writeInt(mLaunchCount);
+ dest.writeInt(mLastEvent);
+ Bundle allCounts = new Bundle();
+ if (mChooserCounts != null) {
+ final int chooserCountSize = mChooserCounts.size();
+ for (int i = 0; i < chooserCountSize; i++) {
+ String action = mChooserCounts.keyAt(i);
+ ArrayMap<String, Integer> counts = mChooserCounts.valueAt(i);
+ Bundle currentCounts = new Bundle();
+ final int annotationSize = counts.size();
+ for (int j = 0; j < annotationSize; j++) {
+ currentCounts.putInt(counts.keyAt(j), counts.valueAt(j));
+ }
+ allCounts.putBundle(action, currentCounts);
+ }
+ }
+ dest.writeBundle(allCounts);
+ }
+
+ public static final Creator<UsageStats> CREATOR = new Creator<UsageStats>() {
+ @Override
+ public UsageStats createFromParcel(Parcel in) {
+ UsageStats stats = new UsageStats();
+ stats.mPackageName = in.readString();
+ stats.mBeginTimeStamp = in.readLong();
+ stats.mEndTimeStamp = in.readLong();
+ stats.mLastTimeUsed = in.readLong();
+ stats.mTotalTimeInForeground = in.readLong();
+ stats.mLaunchCount = in.readInt();
+ stats.mLastEvent = in.readInt();
+ Bundle allCounts = in.readBundle();
+ if (allCounts != null) {
+ stats.mChooserCounts = new ArrayMap<>();
+ for (String action : allCounts.keySet()) {
+ if (!stats.mChooserCounts.containsKey(action)) {
+ ArrayMap<String, Integer> newCounts = new ArrayMap<>();
+ stats.mChooserCounts.put(action, newCounts);
+ }
+ Bundle currentCounts = allCounts.getBundle(action);
+ if (currentCounts != null) {
+ for (String key : currentCounts.keySet()) {
+ int value = currentCounts.getInt(key);
+ if (value > 0) {
+ stats.mChooserCounts.get(action).put(key, value);
+ }
+ }
+ }
+ }
+ }
+ return stats;
+ }
+
+ @Override
+ public UsageStats[] newArray(int size) {
+ return new UsageStats[size];
+ }
+ };
+}
diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java
new file mode 100644
index 00000000..051dccbd
--- /dev/null
+++ b/android/app/usage/UsageStatsManager.java
@@ -0,0 +1,300 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides access to device usage history and statistics. Usage data is aggregated into
+ * time intervals: days, weeks, months, and years.
+ * <p />
+ * When requesting usage data since a particular time, the request might look something like this:
+ * <pre>
+ * PAST REQUEST_TIME TODAY FUTURE
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * YEAR || ¦ |
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * MONTH | || MONTH ¦ |
+ * ——————————————————|—————————||———————————————————————————¦-----------------------|
+ * | WEEK | WEEK|| | WEEK | WE¦EK | WEEK |
+ * ————————————————————————————||———————————————————|———————¦-----------------------|
+ * || |DAY|DAY|DAY|DAY¦DAY|DAY|DAY|DAY|DAY|DAY|
+ * ————————————————————————————||———————————————————————————¦-----------------------|
+ * </pre>
+ * A request for data in the middle of a time interval will include that interval.
+ * <p/>
+ * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS, which
+ * is a system-level permission and will not be granted to third-party apps. However, declaring
+ * the permission implies intention to use the API and the user of the device can grant permission
+ * through the Settings application.
+ */
+@SystemService(Context.USAGE_STATS_SERVICE)
+public final class UsageStatsManager {
+
+ /**
+ * An interval type that spans a day. See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_DAILY = 0;
+
+ /**
+ * An interval type that spans a week. See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_WEEKLY = 1;
+
+ /**
+ * An interval type that spans a month. See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_MONTHLY = 2;
+
+ /**
+ * An interval type that spans a year. See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_YEARLY = 3;
+
+ /**
+ * An interval type that will use the best fit interval for the given time range.
+ * See {@link #queryUsageStats(int, long, long)}.
+ */
+ public static final int INTERVAL_BEST = 4;
+
+ /**
+ * The number of available intervals. Does not include {@link #INTERVAL_BEST}, since it
+ * is a pseudo interval (it actually selects a real interval).
+ * {@hide}
+ */
+ public static final int INTERVAL_COUNT = 4;
+
+ private static final UsageEvents sEmptyResults = new UsageEvents();
+
+ private final Context mContext;
+ private final IUsageStatsManager mService;
+
+ /**
+ * {@hide}
+ */
+ public UsageStatsManager(Context context, IUsageStatsManager service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Gets application usage stats for the given time range, aggregated by the specified interval.
+ * <p>The returned list will contain a {@link UsageStats} object for each package that
+ * has data for an interval that is a subset of the time range given. To illustrate:</p>
+ * <pre>
+ * intervalType = INTERVAL_YEARLY
+ * beginTime = 2013
+ * endTime = 2015 (exclusive)
+ *
+ * Results:
+ * 2013 - com.example.alpha
+ * 2013 - com.example.beta
+ * 2014 - com.example.alpha
+ * 2014 - com.example.beta
+ * 2014 - com.example.charlie
+ * </pre>
+ *
+ * @param intervalType The time interval by which the stats are aggregated.
+ * @param beginTime The inclusive beginning of the range of stats to include in the results.
+ * @param endTime The exclusive end of the range of stats to include in the results.
+ * @return A list of {@link UsageStats} or null if none are available.
+ *
+ * @see #INTERVAL_DAILY
+ * @see #INTERVAL_WEEKLY
+ * @see #INTERVAL_MONTHLY
+ * @see #INTERVAL_YEARLY
+ * @see #INTERVAL_BEST
+ */
+ public List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) {
+ try {
+ @SuppressWarnings("unchecked")
+ ParceledListSlice<UsageStats> slice = mService.queryUsageStats(intervalType, beginTime,
+ endTime, mContext.getOpPackageName());
+ if (slice != null) {
+ return slice.getList();
+ }
+ } catch (RemoteException e) {
+ // fallthrough and return null.
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Gets the hardware configurations the device was in for the given time range, aggregated by
+ * the specified interval. The results are ordered as in
+ * {@link #queryUsageStats(int, long, long)}.
+ *
+ * @param intervalType The time interval by which the stats are aggregated.
+ * @param beginTime The inclusive beginning of the range of stats to include in the results.
+ * @param endTime The exclusive end of the range of stats to include in the results.
+ * @return A list of {@link ConfigurationStats} or null if none are available.
+ */
+ public List<ConfigurationStats> queryConfigurations(int intervalType, long beginTime,
+ long endTime) {
+ try {
+ @SuppressWarnings("unchecked")
+ ParceledListSlice<ConfigurationStats> slice = mService.queryConfigurationStats(
+ intervalType, beginTime, endTime, mContext.getOpPackageName());
+ if (slice != null) {
+ return slice.getList();
+ }
+ } catch (RemoteException e) {
+ // fallthrough and return the empty list.
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Query for events in the given time range. Events are only kept by the system for a few
+ * days.
+ *
+ * @param beginTime The inclusive beginning of the range of events to include in the results.
+ * @param endTime The exclusive end of the range of events to include in the results.
+ * @return A {@link UsageEvents}.
+ */
+ public UsageEvents queryEvents(long beginTime, long endTime) {
+ try {
+ UsageEvents iter = mService.queryEvents(beginTime, endTime,
+ mContext.getOpPackageName());
+ if (iter != null) {
+ return iter;
+ }
+ } catch (RemoteException e) {
+ // fallthrough and return null
+ }
+ return sEmptyResults;
+ }
+
+ /**
+ * A convenience method that queries for all stats in the given range (using the best interval
+ * for that range), merges the resulting data, and keys it by package name.
+ * See {@link #queryUsageStats(int, long, long)}.
+ *
+ * @param beginTime The inclusive beginning of the range of stats to include in the results.
+ * @param endTime The exclusive end of the range of stats to include in the results.
+ * @return A {@link java.util.Map} keyed by package name, or null if no stats are
+ * available.
+ */
+ public Map<String, UsageStats> queryAndAggregateUsageStats(long beginTime, long endTime) {
+ List<UsageStats> stats = queryUsageStats(INTERVAL_BEST, beginTime, endTime);
+ if (stats.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ ArrayMap<String, UsageStats> aggregatedStats = new ArrayMap<>();
+ final int statCount = stats.size();
+ for (int i = 0; i < statCount; i++) {
+ UsageStats newStat = stats.get(i);
+ UsageStats existingStat = aggregatedStats.get(newStat.getPackageName());
+ if (existingStat == null) {
+ aggregatedStats.put(newStat.mPackageName, newStat);
+ } else {
+ existingStat.add(newStat);
+ }
+ }
+ return aggregatedStats;
+ }
+
+ /**
+ * Returns whether the specified app is currently considered inactive. This will be true if the
+ * app hasn't been used directly or indirectly for a period of time defined by the system. This
+ * could be of the order of several hours or days.
+ * @param packageName The package name of the app to query
+ * @return whether the app is currently considered inactive
+ */
+ public boolean isAppInactive(String packageName) {
+ try {
+ return mService.isAppInactive(packageName, UserHandle.myUserId());
+ } catch (RemoteException e) {
+ // fall through and return default
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public void setAppInactive(String packageName, boolean inactive) {
+ try {
+ mService.setAppInactive(packageName, inactive, UserHandle.myUserId());
+ } catch (RemoteException e) {
+ // fall through
+ }
+ }
+
+ /**
+ * {@hide}
+ * Temporarily whitelist the specified app for a short duration. This is to allow an app
+ * receiving a high priority message to be able to access the network and acquire wakelocks
+ * even if the device is in power-save mode or the app is currently considered inactive.
+ * @param packageName The package name of the app to whitelist.
+ * @param duration Duration to whitelist the app for, in milliseconds. It is recommended that
+ * this be limited to 10s of seconds. Requested duration will be clamped to a few minutes.
+ * @param user The user for whom the package should be whitelisted. Passing in a user that is
+ * not the same as the caller's process will require the INTERACT_ACROSS_USERS permission.
+ * @see #isAppInactive(String)
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST)
+ public void whitelistAppTemporarily(String packageName, long duration, UserHandle user) {
+ try {
+ mService.whitelistAppTemporarily(packageName, duration, user.getIdentifier());
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
+ * Inform usage stats that the carrier privileged apps access rules have changed.
+ * @hide
+ */
+ public void onCarrierPrivilegedAppsChanged() {
+ try {
+ mService.onCarrierPrivilegedAppsChanged();
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
+ * Reports a Chooser action to the UsageStatsManager.
+ *
+ * @param packageName The package name of the app that is selected.
+ * @param userId The user id of who makes the selection.
+ * @param contentType The type of the content, e.g., Image, Video, App.
+ * @param annotations The annotations of the content, e.g., Game, Selfie.
+ * @param action The action type of Intent that invokes ChooserActivity.
+ * {@link UsageEvents}
+ * @hide
+ */
+ public void reportChooserSelection(String packageName, int userId, String contentType,
+ String[] annotations, String action) {
+ try {
+ mService.reportChooserSelection(packageName, userId, contentType, annotations, action);
+ } catch (RemoteException re) {
+ }
+ }
+}
diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java
new file mode 100644
index 00000000..dbaace2f
--- /dev/null
+++ b/android/app/usage/UsageStatsManagerInternal.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package android.app.usage;
+
+import android.content.ComponentName;
+import android.content.res.Configuration;
+
+import java.util.List;
+
+/**
+ * UsageStatsManager local system service interface.
+ *
+ * {@hide} Only for use within the system server.
+ */
+public abstract class UsageStatsManagerInternal {
+
+ /**
+ * Reports an event to the UsageStatsManager.
+ *
+ * @param component The component for which this event occurred.
+ * @param userId The user id to which the component belongs to.
+ * @param eventType The event that occurred. Valid values can be found at
+ * {@link UsageEvents}
+ */
+ public abstract void reportEvent(ComponentName component, int userId, int eventType);
+
+ /**
+ * Reports an event to the UsageStatsManager.
+ *
+ * @param packageName The package for which this event occurred.
+ * @param userId The user id to which the component belongs to.
+ * @param eventType The event that occurred. Valid values can be found at
+ * {@link UsageEvents}
+ */
+ public abstract void reportEvent(String packageName, int userId, int eventType);
+
+ /**
+ * Reports a configuration change to the UsageStatsManager.
+ *
+ * @param config The new device configuration.
+ */
+ public abstract void reportConfigurationChange(Configuration config, int userId);
+
+ /**
+ * Reports that an action equivalent to a ShortcutInfo is taken by the user.
+ *
+ * @param packageName The package name of the shortcut publisher
+ * @param shortcutId The ID of the shortcut in question
+ * @param userId The user in which the content provider was accessed.
+ *
+ * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+ */
+ public abstract void reportShortcutUsage(String packageName, String shortcutId, int userId);
+
+ /**
+ * Reports that a content provider has been accessed by a foreground app.
+ * @param name The authority of the content provider
+ * @param pkgName The package name of the content provider
+ * @param userId The user in which the content provider was accessed.
+ */
+ public abstract void reportContentProviderUsage(String name, String pkgName, int userId);
+
+ /**
+ * Prepares the UsageStatsService for shutdown.
+ */
+ public abstract void prepareShutdown();
+
+ /**
+ * Returns true if the app has not been used for a certain amount of time. How much time?
+ * Could be hours, could be days, who knows?
+ *
+ * @param packageName
+ * @param uidForAppId The uid of the app, which will be used for its app id
+ * @param userId
+ * @return
+ */
+ public abstract boolean isAppIdle(String packageName, int uidForAppId, int userId);
+
+ /**
+ * Returns all of the uids for a given user where all packages associating with that uid
+ * are in the app idle state -- there are no associated apps that are not idle. This means
+ * all of the returned uids can be safely considered app idle.
+ */
+ public abstract int[] getIdleUidsForUser(int userId);
+
+ /**
+ * @return True if currently app idle parole mode is on. This means all idle apps are allow to
+ * run for a short period of time.
+ */
+ public abstract boolean isAppIdleParoleOn();
+
+ /**
+ * Sets up a listener for changes to packages being accessed.
+ * @param listener A listener within the system process.
+ */
+ public abstract void addAppIdleStateChangeListener(
+ AppIdleStateChangeListener listener);
+
+ /**
+ * Removes a listener that was previously added for package usage state changes.
+ * @param listener The listener within the system process to remove.
+ */
+ public abstract void removeAppIdleStateChangeListener(
+ AppIdleStateChangeListener listener);
+
+ public static abstract class AppIdleStateChangeListener {
+ public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle);
+ public abstract void onParoleStateChanged(boolean isParoleOn);
+ }
+
+ /* Backup/Restore API */
+ public abstract byte[] getBackupPayload(int user, String key);
+
+ public abstract void applyRestoredPayload(int user, String key, byte[] payload);
+
+ /**
+ * Return usage stats.
+ *
+ * @param obfuscateInstantApps whether instant app package names need to be obfuscated in the
+ * result.
+ */
+ public abstract List<UsageStats> queryUsageStatsForUser(
+ int userId, int interval, long beginTime, long endTime, boolean obfuscateInstantApps);
+}