From 09e9cdceceb722643e2c80c6544d44a43d7f95f0 Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Wed, 6 Nov 2013 14:54:50 -0800 Subject: DO NOT MERGE : Move some system services to their own sub package. (Cherry pick from master) As a next step they can be moved into separate directories to be built as separate modules that may or may not be included in a particular configuration. Moves AppWidgetService, BackupManagerService, ClipboardService, DevicePolicyMS, and WallpaperMS. Change-Id: Idd92871c1828bdde81d85fe99a9c87a22d53169d --- core/java/android/app/IWallpaperManager.aidl | 10 + core/res/AndroidManifest.xml | 2 +- .../java/com/android/server/AppWidgetService.java | 363 -- .../com/android/server/AppWidgetServiceImpl.java | 2126 ------- .../com/android/server/BackupManagerService.java | 6193 ------------------- .../java/com/android/server/ClipboardService.java | 366 -- .../android/server/DevicePolicyManagerService.java | 2942 ---------- .../android/server/PackageManagerBackupAgent.java | 425 -- .../com/android/server/PreferredComponent.java | 234 - .../java/com/android/server/SystemBackupAgent.java | 165 - services/java/com/android/server/SystemServer.java | 5 + .../android/server/WallpaperManagerService.java | 1345 ----- .../android/server/appwidget/AppWidgetService.java | 363 ++ .../server/appwidget/AppWidgetServiceImpl.java | 2126 +++++++ .../server/backup/BackupManagerService.java | 6196 ++++++++++++++++++++ .../server/backup/PackageManagerBackupAgent.java | 425 ++ .../android/server/backup/SystemBackupAgent.java | 185 + .../android/server/clipboard/ClipboardService.java | 366 ++ .../devicepolicy/DevicePolicyManagerService.java | 2942 ++++++++++ .../com/android/server/pm/PreferredActivity.java | 1 - .../com/android/server/pm/PreferredComponent.java | 234 + .../server/wallpaper/WallpaperManagerService.java | 1353 +++++ 22 files changed, 14206 insertions(+), 14161 deletions(-) delete mode 100644 services/java/com/android/server/AppWidgetService.java delete mode 100644 services/java/com/android/server/AppWidgetServiceImpl.java delete mode 100644 services/java/com/android/server/BackupManagerService.java delete mode 100644 services/java/com/android/server/ClipboardService.java delete mode 100644 services/java/com/android/server/DevicePolicyManagerService.java delete mode 100644 services/java/com/android/server/PackageManagerBackupAgent.java delete mode 100644 services/java/com/android/server/PreferredComponent.java delete mode 100644 services/java/com/android/server/SystemBackupAgent.java delete mode 100644 services/java/com/android/server/WallpaperManagerService.java create mode 100644 services/java/com/android/server/appwidget/AppWidgetService.java create mode 100644 services/java/com/android/server/appwidget/AppWidgetServiceImpl.java create mode 100644 services/java/com/android/server/backup/BackupManagerService.java create mode 100644 services/java/com/android/server/backup/PackageManagerBackupAgent.java create mode 100644 services/java/com/android/server/backup/SystemBackupAgent.java create mode 100644 services/java/com/android/server/clipboard/ClipboardService.java create mode 100644 services/java/com/android/server/devicepolicy/DevicePolicyManagerService.java create mode 100644 services/java/com/android/server/pm/PreferredComponent.java create mode 100644 services/java/com/android/server/wallpaper/WallpaperManagerService.java diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 3efd3c0ede8b..181eb63bce69 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -71,4 +71,14 @@ interface IWallpaperManager { * Returns the desired minimum height for the wallpaper. */ int getHeightHint(); + + /** + * Returns the name of the wallpaper. Private API. + */ + String getName(); + + /** + * Informs the service that wallpaper settings have been restored. Private API. + */ + void settingsRestored(); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index b198937df18d..e5461178390b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2522,7 +2522,7 @@ android:hasCode="false" android:label="@string/android_system_label" android:allowClearUserData="false" - android:backupAgent="com.android.server.SystemBackupAgent" + android:backupAgent="com.android.server.backup.SystemBackupAgent" android:killAfterRestore="false" android:icon="@drawable/ic_launcher_android" android:supportsRtl="true"> diff --git a/services/java/com/android/server/AppWidgetService.java b/services/java/com/android/server/AppWidgetService.java deleted file mode 100644 index 203cca692df0..000000000000 --- a/services/java/com/android/server/AppWidgetService.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import android.app.ActivityManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.Slog; -import android.util.SparseArray; -import android.widget.RemoteViews; - -import com.android.internal.appwidget.IAppWidgetHost; -import com.android.internal.appwidget.IAppWidgetService; -import com.android.internal.os.BackgroundThread; -import com.android.internal.util.IndentingPrintWriter; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.List; -import java.util.Locale; - - -/** - * Redirects calls to this service to the instance of the service for the appropriate user. - */ -class AppWidgetService extends IAppWidgetService.Stub -{ - private static final String TAG = "AppWidgetService"; - - Context mContext; - Locale mLocale; - PackageManager mPackageManager; - boolean mSafeMode; - private final Handler mSaveStateHandler; - - private final SparseArray mAppWidgetServices; - - AppWidgetService(Context context) { - mContext = context; - - mSaveStateHandler = BackgroundThread.getHandler(); - - mAppWidgetServices = new SparseArray(5); - AppWidgetServiceImpl primary = new AppWidgetServiceImpl(context, 0, mSaveStateHandler); - mAppWidgetServices.append(0, primary); - } - - public void systemRunning(boolean safeMode) { - mSafeMode = safeMode; - - mAppWidgetServices.get(0).systemReady(safeMode); - - // Register for the boot completed broadcast, so we can send the - // ENABLE broacasts. If we try to send them now, they time out, - // because the system isn't ready to handle them yet. - mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, - new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null); - - // Register for configuration changes so we can update the names - // of the widgets when the locale changes. - mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED), null, null); - - // Register for broadcasts about package install, etc., so we can - // update the provider list. - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, - filter, null, null); - // Register for events related to sdcard installation. - IntentFilter sdFilter = new IntentFilter(); - sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); - mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, - sdFilter, null, null); - - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_REMOVED); - userFilter.addAction(Intent.ACTION_USER_STOPPING); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) { - onUserRemoved(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL)); - } else if (Intent.ACTION_USER_STOPPING.equals(intent.getAction())) { - onUserStopping(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL)); - } - } - }, userFilter); - } - - @Override - public int allocateAppWidgetId(String packageName, int hostId, int userId) - throws RemoteException { - return getImplForUser(userId).allocateAppWidgetId(packageName, hostId); - } - - @Override - public int[] getAppWidgetIdsForHost(int hostId, int userId) throws RemoteException { - return getImplForUser(userId).getAppWidgetIdsForHost(hostId); - } - - @Override - public void deleteAppWidgetId(int appWidgetId, int userId) throws RemoteException { - getImplForUser(userId).deleteAppWidgetId(appWidgetId); - } - - @Override - public void deleteHost(int hostId, int userId) throws RemoteException { - getImplForUser(userId).deleteHost(hostId); - } - - @Override - public void deleteAllHosts(int userId) throws RemoteException { - getImplForUser(userId).deleteAllHosts(); - } - - @Override - public void bindAppWidgetId(int appWidgetId, ComponentName provider, Bundle options, int userId) - throws RemoteException { - getImplForUser(userId).bindAppWidgetId(appWidgetId, provider, options); - } - - @Override - public boolean bindAppWidgetIdIfAllowed( - String packageName, int appWidgetId, ComponentName provider, Bundle options, int userId) - throws RemoteException { - return getImplForUser(userId).bindAppWidgetIdIfAllowed( - packageName, appWidgetId, provider, options); - } - - @Override - public boolean hasBindAppWidgetPermission(String packageName, int userId) - throws RemoteException { - return getImplForUser(userId).hasBindAppWidgetPermission(packageName); - } - - @Override - public void setBindAppWidgetPermission(String packageName, boolean permission, int userId) - throws RemoteException { - getImplForUser(userId).setBindAppWidgetPermission(packageName, permission); - } - - @Override - public void bindRemoteViewsService(int appWidgetId, Intent intent, IBinder connection, - int userId) throws RemoteException { - getImplForUser(userId).bindRemoteViewsService(appWidgetId, intent, connection); - } - - @Override - public int[] startListening(IAppWidgetHost host, String packageName, int hostId, - List updatedViews, int userId) throws RemoteException { - return getImplForUser(userId).startListening(host, packageName, hostId, updatedViews); - } - - public void onUserRemoved(int userId) { - if (userId < 1) return; - synchronized (mAppWidgetServices) { - AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); - mAppWidgetServices.remove(userId); - - if (impl == null) { - AppWidgetServiceImpl.getSettingsFile(userId).delete(); - } else { - impl.onUserRemoved(); - } - } - } - - public void onUserStopping(int userId) { - if (userId < 1) return; - synchronized (mAppWidgetServices) { - AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); - if (impl != null) { - mAppWidgetServices.remove(userId); - impl.onUserStopping(); - } - } - } - - private void checkPermission(int userId) { - int realUserId = ActivityManager.handleIncomingUser( - Binder.getCallingPid(), - Binder.getCallingUid(), - userId, - false, /* allowAll */ - true, /* requireFull */ - this.getClass().getSimpleName(), - this.getClass().getPackage().getName()); - } - - private AppWidgetServiceImpl getImplForUser(int userId) { - checkPermission(userId); - boolean sendInitial = false; - AppWidgetServiceImpl service; - synchronized (mAppWidgetServices) { - service = mAppWidgetServices.get(userId); - if (service == null) { - Slog.i(TAG, "Unable to find AppWidgetServiceImpl for user " + userId + ", adding"); - // TODO: Verify that it's a valid user - service = new AppWidgetServiceImpl(mContext, userId, mSaveStateHandler); - service.systemReady(mSafeMode); - // Assume that BOOT_COMPLETED was received, as this is a non-primary user. - mAppWidgetServices.append(userId, service); - sendInitial = true; - } - } - if (sendInitial) { - service.sendInitialBroadcasts(); - } - return service; - } - - @Override - public int[] getAppWidgetIds(ComponentName provider, int userId) throws RemoteException { - return getImplForUser(userId).getAppWidgetIds(provider); - } - - @Override - public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId, int userId) - throws RemoteException { - return getImplForUser(userId).getAppWidgetInfo(appWidgetId); - } - - @Override - public RemoteViews getAppWidgetViews(int appWidgetId, int userId) throws RemoteException { - return getImplForUser(userId).getAppWidgetViews(appWidgetId); - } - - @Override - public void updateAppWidgetOptions(int appWidgetId, Bundle options, int userId) { - getImplForUser(userId).updateAppWidgetOptions(appWidgetId, options); - } - - @Override - public Bundle getAppWidgetOptions(int appWidgetId, int userId) { - return getImplForUser(userId).getAppWidgetOptions(appWidgetId); - } - - @Override - public List getInstalledProviders(int categoryFilter, int userId) - throws RemoteException { - return getImplForUser(userId).getInstalledProviders(categoryFilter); - } - - @Override - public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId, int userId) - throws RemoteException { - getImplForUser(userId).notifyAppWidgetViewDataChanged( - appWidgetIds, viewId); - } - - @Override - public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) - throws RemoteException { - getImplForUser(userId).partiallyUpdateAppWidgetIds( - appWidgetIds, views); - } - - @Override - public void stopListening(int hostId, int userId) throws RemoteException { - getImplForUser(userId).stopListening(hostId); - } - - @Override - public void unbindRemoteViewsService(int appWidgetId, Intent intent, int userId) - throws RemoteException { - getImplForUser(userId).unbindRemoteViewsService( - appWidgetId, intent); - } - - @Override - public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) - throws RemoteException { - getImplForUser(userId).updateAppWidgetIds(appWidgetIds, views); - } - - @Override - public void updateAppWidgetProvider(ComponentName provider, RemoteViews views, int userId) - throws RemoteException { - getImplForUser(userId).updateAppWidgetProvider(provider, views); - } - - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); - - // Dump the state of all the app widget providers - synchronized (mAppWidgetServices) { - IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - for (int i = 0; i < mAppWidgetServices.size(); i++) { - pw.println("User: " + mAppWidgetServices.keyAt(i)); - ipw.increaseIndent(); - AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); - service.dump(fd, ipw, args); - ipw.decreaseIndent(); - } - } - } - - BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - // Slog.d(TAG, "received " + action); - if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId >= 0) { - getImplForUser(userId).sendInitialBroadcasts(); - } else { - Slog.w(TAG, "Incorrect user handle supplied in " + intent); - } - } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { - for (int i = 0; i < mAppWidgetServices.size(); i++) { - AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); - service.onConfigurationChanged(); - } - } else { - int sendingUser = getSendingUserId(); - if (sendingUser == UserHandle.USER_ALL) { - for (int i = 0; i < mAppWidgetServices.size(); i++) { - AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); - service.onBroadcastReceived(intent); - } - } else { - AppWidgetServiceImpl service = mAppWidgetServices.get(sendingUser); - if (service != null) { - service.onBroadcastReceived(intent); - } - } - } - } - }; -} diff --git a/services/java/com/android/server/AppWidgetServiceImpl.java b/services/java/com/android/server/AppWidgetServiceImpl.java deleted file mode 100644 index 69ae846597f7..000000000000 --- a/services/java/com/android/server/AppWidgetServiceImpl.java +++ /dev/null @@ -1,2126 +0,0 @@ -/* - * 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 com.android.server; - -import android.app.AlarmManager; -import android.app.AppGlobals; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.Intent.FilterComparison; -import android.content.ServiceConnection; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.graphics.Point; -import android.net.Uri; -import android.os.Binder; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Process; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.AtomicFile; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Pair; -import android.util.Slog; -import android.util.TypedValue; -import android.util.Xml; -import android.view.Display; -import android.view.WindowManager; -import android.widget.RemoteViews; - -import com.android.internal.appwidget.IAppWidgetHost; -import com.android.internal.util.FastXmlSerializer; -import com.android.internal.widget.IRemoteViewsAdapterConnection; -import com.android.internal.widget.IRemoteViewsFactory; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -class AppWidgetServiceImpl { - - private static final String KEYGUARD_HOST_PACKAGE = "com.android.keyguard"; - private static final int KEYGUARD_HOST_ID = 0x4b455947; - private static final String TAG = "AppWidgetServiceImpl"; - private static final String SETTINGS_FILENAME = "appwidgets.xml"; - private static final int MIN_UPDATE_PERIOD = 30 * 60 * 1000; // 30 minutes - private static final int CURRENT_VERSION = 1; // Bump if the stored widgets need to be upgraded. - - private static boolean DBG = false; - - /* - * When identifying a Host or Provider based on the calling process, use the uid field. When - * identifying a Host or Provider based on a package manager broadcast, use the package given. - */ - - static class Provider { - int uid; - AppWidgetProviderInfo info; - ArrayList instances = new ArrayList(); - PendingIntent broadcast; - boolean zombie; // if we're in safe mode, don't prune this just because nobody references it - - int tag; // for use while saving state (the index) - } - - static class Host { - int uid; - int hostId; - String packageName; - ArrayList instances = new ArrayList(); - IAppWidgetHost callbacks; - boolean zombie; // if we're in safe mode, don't prune this just because nobody references it - - int tag; // for use while saving state (the index) - - boolean uidMatches(int callingUid) { - if (UserHandle.getAppId(callingUid) == Process.myUid()) { - // For a host that's in the system process, ignore the user id - return UserHandle.isSameApp(this.uid, callingUid); - } else { - return this.uid == callingUid; - } - } - } - - static class AppWidgetId { - int appWidgetId; - Provider provider; - RemoteViews views; - Bundle options; - Host host; - } - - /** - * Acts as a proxy between the ServiceConnection and the RemoteViewsAdapterConnection. This - * needs to be a static inner class since a reference to the ServiceConnection is held globally - * and may lead us to leak AppWidgetService instances (if there were more than one). - */ - static class ServiceConnectionProxy implements ServiceConnection { - private final IBinder mConnectionCb; - - ServiceConnectionProxy(Pair key, IBinder connectionCb) { - mConnectionCb = connectionCb; - } - - public void onServiceConnected(ComponentName name, IBinder service) { - final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub - .asInterface(mConnectionCb); - try { - cb.onServiceConnected(service); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void onServiceDisconnected(ComponentName name) { - disconnect(); - } - - public void disconnect() { - final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub - .asInterface(mConnectionCb); - try { - cb.onServiceDisconnected(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - // Manages active connections to RemoteViewsServices - private final HashMap, ServiceConnection> mBoundRemoteViewsServices = new HashMap, ServiceConnection>(); - // Manages persistent references to RemoteViewsServices from different App Widgets - private final HashMap> mRemoteViewsServicesAppWidgets = new HashMap>(); - - final Context mContext; - final IPackageManager mPm; - final AlarmManager mAlarmManager; - final ArrayList mInstalledProviders = new ArrayList(); - final int mUserId; - final boolean mHasFeature; - - Locale mLocale; - int mNextAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + 1; - final ArrayList mAppWidgetIds = new ArrayList(); - final ArrayList mHosts = new ArrayList(); - // set of package names - final HashSet mPackagesWithBindWidgetPermission = new HashSet(); - boolean mSafeMode; - boolean mStateLoaded; - int mMaxWidgetBitmapMemory; - - private final Handler mSaveStateHandler; - - // These are for debugging only -- widgets are going missing in some rare instances - ArrayList mDeletedProviders = new ArrayList(); - ArrayList mDeletedHosts = new ArrayList(); - - AppWidgetServiceImpl(Context context, int userId, Handler saveStateHandler) { - mContext = context; - mPm = AppGlobals.getPackageManager(); - mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - mUserId = userId; - mSaveStateHandler = saveStateHandler; - mHasFeature = context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_APP_WIDGETS); - computeMaximumWidgetBitmapMemory(); - } - - void computeMaximumWidgetBitmapMemory() { - WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - Point size = new Point(); - display.getRealSize(size); - // Cap memory usage at 1.5 times the size of the display - // 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h - mMaxWidgetBitmapMemory = 6 * size.x * size.y; - } - - public void systemReady(boolean safeMode) { - mSafeMode = safeMode; - - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - } - } - - private void log(String msg) { - Slog.i(TAG, "u=" + mUserId + ": " + msg); - } - - void onConfigurationChanged() { - if (DBG) log("Got onConfigurationChanged()"); - Locale revised = Locale.getDefault(); - if (revised == null || mLocale == null || !(revised.equals(mLocale))) { - mLocale = revised; - - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - // Note: updateProvidersForPackageLocked() may remove providers, so we must copy the - // list of installed providers and skip providers that we don't need to update. - // Also note that remove the provider does not clear the Provider component data. - ArrayList installedProviders = - new ArrayList(mInstalledProviders); - HashSet removedProviders = new HashSet(); - int N = installedProviders.size(); - for (int i = N - 1; i >= 0; i--) { - Provider p = installedProviders.get(i); - ComponentName cn = p.info.provider; - if (!removedProviders.contains(cn)) { - updateProvidersForPackageLocked(cn.getPackageName(), removedProviders); - } - } - saveStateAsync(); - } - } - } - - void onBroadcastReceived(Intent intent) { - if (DBG) log("onBroadcast " + intent); - final String action = intent.getAction(); - boolean added = false; - boolean changed = false; - boolean providersModified = false; - String pkgList[] = null; - if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { - pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - added = true; - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { - pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - added = false; - } else { - Uri uri = intent.getData(); - if (uri == null) { - return; - } - String pkgName = uri.getSchemeSpecificPart(); - if (pkgName == null) { - return; - } - pkgList = new String[] { pkgName }; - added = Intent.ACTION_PACKAGE_ADDED.equals(action); - changed = Intent.ACTION_PACKAGE_CHANGED.equals(action); - } - if (pkgList == null || pkgList.length == 0) { - return; - } - if (added || changed) { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - Bundle extras = intent.getExtras(); - if (changed - || (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false))) { - for (String pkgName : pkgList) { - // The package was just upgraded - providersModified |= updateProvidersForPackageLocked(pkgName, null); - } - } else { - // The package was just added - for (String pkgName : pkgList) { - providersModified |= addProvidersForPackageLocked(pkgName); - } - } - saveStateAsync(); - } - } else { - Bundle extras = intent.getExtras(); - if (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false)) { - // The package is being updated. We'll receive a PACKAGE_ADDED shortly. - } else { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - for (String pkgName : pkgList) { - providersModified |= removeProvidersForPackageLocked(pkgName); - saveStateAsync(); - } - } - } - } - - if (providersModified) { - // If the set of providers has been modified, notify each active AppWidgetHost - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - notifyHostsForProvidersChangedLocked(); - } - } - } - - private void dumpProvider(Provider p, int index, PrintWriter pw) { - AppWidgetProviderInfo info = p.info; - pw.print(" ["); pw.print(index); pw.print("] provider "); - pw.print(info.provider.flattenToShortString()); - pw.println(':'); - pw.print(" min=("); pw.print(info.minWidth); - pw.print("x"); pw.print(info.minHeight); - pw.print(") minResize=("); pw.print(info.minResizeWidth); - pw.print("x"); pw.print(info.minResizeHeight); - pw.print(") updatePeriodMillis="); - pw.print(info.updatePeriodMillis); - pw.print(" resizeMode="); - pw.print(info.resizeMode); - pw.print(info.widgetCategory); - pw.print(" autoAdvanceViewId="); - pw.print(info.autoAdvanceViewId); - pw.print(" initialLayout=#"); - pw.print(Integer.toHexString(info.initialLayout)); - pw.print(" uid="); pw.print(p.uid); - pw.print(" zombie="); pw.println(p.zombie); - } - - private void dumpHost(Host host, int index, PrintWriter pw) { - pw.print(" ["); pw.print(index); pw.print("] hostId="); - pw.print(host.hostId); pw.print(' '); - pw.print(host.packageName); pw.print('/'); - pw.print(host.uid); pw.println(':'); - pw.print(" callbacks="); pw.println(host.callbacks); - pw.print(" instances.size="); pw.print(host.instances.size()); - pw.print(" zombie="); pw.println(host.zombie); - } - - private void dumpAppWidgetId(AppWidgetId id, int index, PrintWriter pw) { - pw.print(" ["); pw.print(index); pw.print("] id="); - pw.println(id.appWidgetId); - pw.print(" hostId="); - pw.print(id.host.hostId); pw.print(' '); - pw.print(id.host.packageName); pw.print('/'); - pw.println(id.host.uid); - if (id.provider != null) { - pw.print(" provider="); - pw.println(id.provider.info.provider.flattenToShortString()); - } - if (id.host != null) { - pw.print(" host.callbacks="); pw.println(id.host.callbacks); - } - if (id.views != null) { - pw.print(" views="); pw.println(id.views); - } - } - - void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) - != PackageManager.PERMISSION_GRANTED) { - pw.println("Permission Denial: can't dump from from pid=" - + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return; - } - - synchronized (mAppWidgetIds) { - int N = mInstalledProviders.size(); - pw.println("Providers:"); - for (int i=0; i key = Pair.create(appWidgetId, fc); - if (mBoundRemoteViewsServices.containsKey(key)) { - conn = (ServiceConnectionProxy) mBoundRemoteViewsServices.get(key); - conn.disconnect(); - mContext.unbindService(conn); - mBoundRemoteViewsServices.remove(key); - } - - int userId = UserHandle.getUserId(id.provider.uid); - if (userId != mUserId) { - Slog.w(TAG, "AppWidgetServiceImpl of user " + mUserId - + " binding to provider on user " + userId); - } - // Bind to the RemoteViewsService (which will trigger a callback to the - // RemoteViewsAdapter.onServiceConnected()) - final long token = Binder.clearCallingIdentity(); - try { - conn = new ServiceConnectionProxy(key, connection); - mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, - new UserHandle(userId)); - mBoundRemoteViewsServices.put(key, conn); - } finally { - Binder.restoreCallingIdentity(token); - } - - // Add it to the mapping of RemoteViewsService to appWidgetIds so that we can determine - // when we can call back to the RemoteViewsService later to destroy associated - // factories. - incrementAppWidgetServiceRefCount(appWidgetId, fc); - } - } - - // Unbinds from a specific RemoteViewsService - public void unbindRemoteViewsService(int appWidgetId, Intent intent) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return; - } - ensureStateLoadedLocked(); - // Unbind from the RemoteViewsService (which will trigger a callback to the bound - // RemoteViewsAdapter) - Pair key = Pair.create(appWidgetId, new FilterComparison( - intent)); - if (mBoundRemoteViewsServices.containsKey(key)) { - // We don't need to use the appWidgetId until after we are sure there is something - // to unbind. Note that this may mask certain issues with apps calling unbind() - // more than necessary. - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); - if (id == null) { - throw new IllegalArgumentException("bad appWidgetId"); - } - - ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices - .get(key); - conn.disconnect(); - mContext.unbindService(conn); - mBoundRemoteViewsServices.remove(key); - } - } - } - - // Unbinds from a RemoteViewsService when we delete an app widget - private void unbindAppWidgetRemoteViewsServicesLocked(AppWidgetId id) { - int appWidgetId = id.appWidgetId; - // Unbind all connections to Services bound to this AppWidgetId - Iterator> it = mBoundRemoteViewsServices.keySet() - .iterator(); - while (it.hasNext()) { - final Pair key = it.next(); - if (key.first.intValue() == appWidgetId) { - final ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices - .get(key); - conn.disconnect(); - mContext.unbindService(conn); - it.remove(); - } - } - - // Check if we need to destroy any services (if no other app widgets are - // referencing the same service) - decrementAppWidgetServiceRefCount(id); - } - - // Destroys the cached factory on the RemoteViewsService's side related to the specified intent - private void destroyRemoteViewsService(final Intent intent, AppWidgetId id) { - final ServiceConnection conn = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - final IRemoteViewsFactory cb = IRemoteViewsFactory.Stub.asInterface(service); - try { - cb.onDestroy(intent); - } catch (RemoteException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } - mContext.unbindService(this); - } - - @Override - public void onServiceDisconnected(android.content.ComponentName name) { - // Do nothing - } - }; - - int userId = UserHandle.getUserId(id.provider.uid); - // Bind to the service and remove the static intent->factory mapping in the - // RemoteViewsService. - final long token = Binder.clearCallingIdentity(); - try { - mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, - new UserHandle(userId)); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - // Adds to the ref-count for a given RemoteViewsService intent - private void incrementAppWidgetServiceRefCount(int appWidgetId, FilterComparison fc) { - HashSet appWidgetIds = null; - if (mRemoteViewsServicesAppWidgets.containsKey(fc)) { - appWidgetIds = mRemoteViewsServicesAppWidgets.get(fc); - } else { - appWidgetIds = new HashSet(); - mRemoteViewsServicesAppWidgets.put(fc, appWidgetIds); - } - appWidgetIds.add(appWidgetId); - } - - // Subtracts from the ref-count for a given RemoteViewsService intent, prompting a delete if - // the ref-count reaches zero. - private void decrementAppWidgetServiceRefCount(AppWidgetId id) { - Iterator it = mRemoteViewsServicesAppWidgets.keySet().iterator(); - while (it.hasNext()) { - final FilterComparison key = it.next(); - final HashSet ids = mRemoteViewsServicesAppWidgets.get(key); - if (ids.remove(id.appWidgetId)) { - // If we have removed the last app widget referencing this service, then we - // should destroy it and remove it from this set - if (ids.isEmpty()) { - destroyRemoteViewsService(key.getIntent(), id); - it.remove(); - } - } - } - } - - public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return null; - } - ensureStateLoadedLocked(); - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); - if (id != null && id.provider != null && !id.provider.zombie) { - return cloneIfLocalBinder(id.provider.info); - } - return null; - } - } - - public RemoteViews getAppWidgetViews(int appWidgetId) { - if (DBG) log("getAppWidgetViews id=" + appWidgetId); - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return null; - } - ensureStateLoadedLocked(); - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); - if (id != null) { - return cloneIfLocalBinder(id.views); - } - if (DBG) log(" couldn't find appwidgetid"); - return null; - } - } - - public List getInstalledProviders(int categoryFilter) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return new ArrayList(0); - } - ensureStateLoadedLocked(); - final int N = mInstalledProviders.size(); - ArrayList result = new ArrayList(N); - for (int i = 0; i < N; i++) { - Provider p = mInstalledProviders.get(i); - if (!p.zombie && (p.info.widgetCategory & categoryFilter) != 0) { - result.add(cloneIfLocalBinder(p.info)); - } - } - return result; - } - } - - public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { - if (!mHasFeature) { - return; - } - if (appWidgetIds == null) { - return; - } - if (DBG) log("updateAppWidgetIds views: " + views); - int bitmapMemoryUsage = 0; - if (views != null) { - bitmapMemoryUsage = views.estimateMemoryUsage(); - } - if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) { - throw new IllegalArgumentException("RemoteViews for widget update exceeds maximum" + - " bitmap memory usage (used: " + bitmapMemoryUsage + ", max: " + - mMaxWidgetBitmapMemory + ") The total memory cannot exceed that required to" + - " fill the device's screen once."); - } - - if (appWidgetIds.length == 0) { - return; - } - final int N = appWidgetIds.length; - - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - for (int i = 0; i < N; i++) { - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); - updateAppWidgetInstanceLocked(id, views); - } - } - } - - private void saveStateAsync() { - mSaveStateHandler.post(mSaveStateRunnable); - } - - private final Runnable mSaveStateRunnable = new Runnable() { - @Override - public void run() { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - saveStateLocked(); - } - } - }; - - public void updateAppWidgetOptions(int appWidgetId, Bundle options) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return; - } - options = cloneIfLocalBinder(options); - ensureStateLoadedLocked(); - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); - - if (id == null) { - return; - } - - Provider p = id.provider; - // Merge the options - id.options.putAll(options); - - // send the broacast saying that this appWidgetId has been deleted - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED); - intent.setComponent(p.info.provider); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id.appWidgetId); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, id.options); - mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); - saveStateAsync(); - } - } - - public Bundle getAppWidgetOptions(int appWidgetId) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return Bundle.EMPTY; - } - ensureStateLoadedLocked(); - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); - if (id != null && id.options != null) { - return cloneIfLocalBinder(id.options); - } else { - return Bundle.EMPTY; - } - } - } - - public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { - if (!mHasFeature) { - return; - } - if (appWidgetIds == null) { - return; - } - if (appWidgetIds.length == 0) { - return; - } - final int N = appWidgetIds.length; - - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - for (int i = 0; i < N; i++) { - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); - if (id == null) { - Slog.w(TAG, "widget id " + appWidgetIds[i] + " not found!"); - } else if (id.views != null) { - // Only trigger a partial update for a widget if it has received a full update - updateAppWidgetInstanceLocked(id, views, true); - } - } - } - } - - public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { - if (!mHasFeature) { - return; - } - if (appWidgetIds == null) { - return; - } - if (appWidgetIds.length == 0) { - return; - } - final int N = appWidgetIds.length; - - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - for (int i = 0; i < N; i++) { - AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); - notifyAppWidgetViewDataChangedInstanceLocked(id, viewId); - } - } - } - - public void updateAppWidgetProvider(ComponentName provider, RemoteViews views) { - if (!mHasFeature) { - return; - } - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - Provider p = lookupProviderLocked(provider); - if (p == null) { - Slog.w(TAG, "updateAppWidgetProvider: provider doesn't exist: " + provider); - return; - } - ArrayList instances = p.instances; - final int callingUid = Binder.getCallingUid(); - final int N = instances.size(); - for (int i = 0; i < N; i++) { - AppWidgetId id = instances.get(i); - if (canAccessAppWidgetId(id, callingUid)) { - updateAppWidgetInstanceLocked(id, views); - } - } - } - } - - void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views) { - updateAppWidgetInstanceLocked(id, views, false); - } - - void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views, boolean isPartialUpdate) { - // allow for stale appWidgetIds and other badness - // lookup also checks that the calling process can access the appWidgetId - // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) - if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { - - if (!isPartialUpdate) { - // For a full update we replace the RemoteViews completely. - id.views = views; - } else { - // For a partial update, we merge the new RemoteViews with the old. - id.views.mergeRemoteViews(views); - } - - // is anyone listening? - if (id.host.callbacks != null) { - try { - // the lock is held, but this is a oneway call - id.host.callbacks.updateAppWidget(id.appWidgetId, views, mUserId); - } catch (RemoteException e) { - // It failed; remove the callback. No need to prune because - // we know that this host is still referenced by this instance. - id.host.callbacks = null; - } - } - } - } - - void notifyAppWidgetViewDataChangedInstanceLocked(AppWidgetId id, int viewId) { - // allow for stale appWidgetIds and other badness - // lookup also checks that the calling process can access the appWidgetId - // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) - if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { - // is anyone listening? - if (id.host.callbacks != null) { - try { - // the lock is held, but this is a oneway call - id.host.callbacks.viewDataChanged(id.appWidgetId, viewId, mUserId); - } catch (RemoteException e) { - // It failed; remove the callback. No need to prune because - // we know that this host is still referenced by this instance. - id.host.callbacks = null; - } - } - - // If the host is unavailable, then we call the associated - // RemoteViewsFactory.onDataSetChanged() directly - if (id.host.callbacks == null) { - Set keys = mRemoteViewsServicesAppWidgets.keySet(); - for (FilterComparison key : keys) { - if (mRemoteViewsServicesAppWidgets.get(key).contains(id.appWidgetId)) { - Intent intent = key.getIntent(); - - final ServiceConnection conn = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - IRemoteViewsFactory cb = IRemoteViewsFactory.Stub - .asInterface(service); - try { - cb.onDataSetChangedAsync(); - } catch (RemoteException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } - mContext.unbindService(this); - } - - @Override - public void onServiceDisconnected(android.content.ComponentName name) { - // Do nothing - } - }; - - int userId = UserHandle.getUserId(id.provider.uid); - // Bind to the service and call onDataSetChanged() - final long token = Binder.clearCallingIdentity(); - try { - mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, - new UserHandle(userId)); - } finally { - Binder.restoreCallingIdentity(token); - } - } - } - } - } - } - - private boolean isLocalBinder() { - return Process.myPid() == Binder.getCallingPid(); - } - - private RemoteViews cloneIfLocalBinder(RemoteViews rv) { - if (isLocalBinder() && rv != null) { - return rv.clone(); - } - return rv; - } - - private AppWidgetProviderInfo cloneIfLocalBinder(AppWidgetProviderInfo info) { - if (isLocalBinder() && info != null) { - return info.clone(); - } - return info; - } - - private Bundle cloneIfLocalBinder(Bundle bundle) { - // Note: this is only a shallow copy. For now this will be fine, but it could be problematic - // if we start adding objects to the options. Further, it would only be an issue if keyguard - // used such options. - if (isLocalBinder() && bundle != null) { - return (Bundle) bundle.clone(); - } - return bundle; - } - - public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId, - List updatedViews) { - if (!mHasFeature) { - return new int[0]; - } - int callingUid = enforceCallingUid(packageName); - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - Host host = lookupOrAddHostLocked(callingUid, packageName, hostId); - host.callbacks = callbacks; - - updatedViews.clear(); - - ArrayList instances = host.instances; - int N = instances.size(); - int[] updatedIds = new int[N]; - for (int i = 0; i < N; i++) { - AppWidgetId id = instances.get(i); - updatedIds[i] = id.appWidgetId; - updatedViews.add(cloneIfLocalBinder(id.views)); - } - return updatedIds; - } - } - - public void stopListening(int hostId) { - synchronized (mAppWidgetIds) { - if (!mHasFeature) { - return; - } - ensureStateLoadedLocked(); - Host host = lookupHostLocked(Binder.getCallingUid(), hostId); - if (host != null) { - host.callbacks = null; - pruneHostLocked(host); - } - } - } - - boolean canAccessAppWidgetId(AppWidgetId id, int callingUid) { - if (id.host.uidMatches(callingUid)) { - // Apps hosting the AppWidget have access to it. - return true; - } - if (id.provider != null && id.provider.uid == callingUid) { - // Apps providing the AppWidget have access to it (if the appWidgetId has been bound) - return true; - } - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.BIND_APPWIDGET) == PackageManager.PERMISSION_GRANTED) { - // Apps that can bind have access to all appWidgetIds. - return true; - } - // Nobody else can access it. - return false; - } - - AppWidgetId lookupAppWidgetIdLocked(int appWidgetId) { - int callingUid = Binder.getCallingUid(); - final int N = mAppWidgetIds.size(); - for (int i = 0; i < N; i++) { - AppWidgetId id = mAppWidgetIds.get(i); - if (id.appWidgetId == appWidgetId && canAccessAppWidgetId(id, callingUid)) { - return id; - } - } - return null; - } - - Provider lookupProviderLocked(ComponentName provider) { - final int N = mInstalledProviders.size(); - for (int i = 0; i < N; i++) { - Provider p = mInstalledProviders.get(i); - if (p.info.provider.equals(provider)) { - return p; - } - } - return null; - } - - Host lookupHostLocked(int uid, int hostId) { - final int N = mHosts.size(); - for (int i = 0; i < N; i++) { - Host h = mHosts.get(i); - if (h.uidMatches(uid) && h.hostId == hostId) { - return h; - } - } - return null; - } - - Host lookupOrAddHostLocked(int uid, String packageName, int hostId) { - final int N = mHosts.size(); - for (int i = 0; i < N; i++) { - Host h = mHosts.get(i); - if (h.hostId == hostId && h.packageName.equals(packageName)) { - return h; - } - } - Host host = new Host(); - host.packageName = packageName; - host.uid = uid; - host.hostId = hostId; - mHosts.add(host); - return host; - } - - void pruneHostLocked(Host host) { - if (host.instances.size() == 0 && host.callbacks == null) { - mHosts.remove(host); - } - } - - void loadAppWidgetListLocked() { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - try { - List broadcastReceivers = mPm.queryIntentReceivers(intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - PackageManager.GET_META_DATA, mUserId); - - final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); - for (int i = 0; i < N; i++) { - ResolveInfo ri = broadcastReceivers.get(i); - addProviderLocked(ri); - } - } catch (RemoteException re) { - // Shouldn't happen, local call - } - } - - boolean addProviderLocked(ResolveInfo ri) { - if ((ri.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { - return false; - } - if (!ri.activityInfo.isEnabled()) { - return false; - } - Provider p = parseProviderInfoXml(new ComponentName(ri.activityInfo.packageName, - ri.activityInfo.name), ri); - if (p != null) { - mInstalledProviders.add(p); - return true; - } else { - return false; - } - } - - void removeProviderLocked(int index, Provider p) { - int N = p.instances.size(); - for (int i = 0; i < N; i++) { - AppWidgetId id = p.instances.get(i); - // Call back with empty RemoteViews - updateAppWidgetInstanceLocked(id, null); - // Stop telling the host about updates for this from now on - cancelBroadcasts(p); - // clear out references to this appWidgetId - id.host.instances.remove(id); - mAppWidgetIds.remove(id); - id.provider = null; - pruneHostLocked(id.host); - id.host = null; - } - p.instances.clear(); - mInstalledProviders.remove(index); - mDeletedProviders.add(p); - // no need to send the DISABLE broadcast, since the receiver is gone anyway - cancelBroadcasts(p); - } - - void sendEnableIntentLocked(Provider p) { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED); - intent.setComponent(p.info.provider); - mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); - } - - void sendUpdateIntentLocked(Provider p, int[] appWidgetIds) { - if (appWidgetIds != null && appWidgetIds.length > 0) { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); - intent.setComponent(p.info.provider); - mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); - } - } - - void registerForBroadcastsLocked(Provider p, int[] appWidgetIds) { - if (p.info.updatePeriodMillis > 0) { - // if this is the first instance, set the alarm. otherwise, - // rely on the fact that we've already set it and that - // PendingIntent.getBroadcast will update the extras. - boolean alreadyRegistered = p.broadcast != null; - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); - intent.setComponent(p.info.provider); - long token = Binder.clearCallingIdentity(); - try { - p.broadcast = PendingIntent.getBroadcastAsUser(mContext, 1, intent, - PendingIntent.FLAG_UPDATE_CURRENT, new UserHandle(mUserId)); - } finally { - Binder.restoreCallingIdentity(token); - } - if (!alreadyRegistered) { - long period = p.info.updatePeriodMillis; - if (period < MIN_UPDATE_PERIOD) { - period = MIN_UPDATE_PERIOD; - } - mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock - .elapsedRealtime() - + period, period, p.broadcast); - } - } - } - - static int[] getAppWidgetIds(Provider p) { - int instancesSize = p.instances.size(); - int appWidgetIds[] = new int[instancesSize]; - for (int i = 0; i < instancesSize; i++) { - appWidgetIds[i] = p.instances.get(i).appWidgetId; - } - return appWidgetIds; - } - - public int[] getAppWidgetIds(ComponentName provider) { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - Provider p = lookupProviderLocked(provider); - if (p != null && Binder.getCallingUid() == p.uid) { - return getAppWidgetIds(p); - } else { - return new int[0]; - } - } - } - - static int[] getAppWidgetIds(Host h) { - int instancesSize = h.instances.size(); - int appWidgetIds[] = new int[instancesSize]; - for (int i = 0; i < instancesSize; i++) { - appWidgetIds[i] = h.instances.get(i).appWidgetId; - } - return appWidgetIds; - } - - public int[] getAppWidgetIdsForHost(int hostId) { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - int callingUid = Binder.getCallingUid(); - Host host = lookupHostLocked(callingUid, hostId); - if (host != null) { - return getAppWidgetIds(host); - } else { - return new int[0]; - } - } - } - - private Provider parseProviderInfoXml(ComponentName component, ResolveInfo ri) { - Provider p = null; - - ActivityInfo activityInfo = ri.activityInfo; - XmlResourceParser parser = null; - try { - parser = activityInfo.loadXmlMetaData(mContext.getPackageManager(), - AppWidgetManager.META_DATA_APPWIDGET_PROVIDER); - if (parser == null) { - Slog.w(TAG, "No " + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER - + " meta-data for " + "AppWidget provider '" + component + '\''); - return null; - } - - AttributeSet attrs = Xml.asAttributeSet(parser); - - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - // drain whitespace, comments, etc. - } - - String nodeName = parser.getName(); - if (!"appwidget-provider".equals(nodeName)) { - Slog.w(TAG, "Meta-data does not start with appwidget-provider tag for" - + " AppWidget provider '" + component + '\''); - return null; - } - - p = new Provider(); - AppWidgetProviderInfo info = p.info = new AppWidgetProviderInfo(); - info.provider = component; - p.uid = activityInfo.applicationInfo.uid; - - Resources res = mContext.getPackageManager() - .getResourcesForApplicationAsUser(activityInfo.packageName, mUserId); - - TypedArray sa = res.obtainAttributes(attrs, - com.android.internal.R.styleable.AppWidgetProviderInfo); - - // These dimensions has to be resolved in the application's context. - // We simply send back the raw complex data, which will be - // converted to dp in {@link AppWidgetManager#getAppWidgetInfo}. - TypedValue value = sa - .peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minWidth); - info.minWidth = value != null ? value.data : 0; - value = sa.peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minHeight); - info.minHeight = value != null ? value.data : 0; - value = sa.peekValue( - com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeWidth); - info.minResizeWidth = value != null ? value.data : info.minWidth; - value = sa.peekValue( - com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeHeight); - info.minResizeHeight = value != null ? value.data : info.minHeight; - info.updatePeriodMillis = sa.getInt( - com.android.internal.R.styleable.AppWidgetProviderInfo_updatePeriodMillis, 0); - info.initialLayout = sa.getResourceId( - com.android.internal.R.styleable.AppWidgetProviderInfo_initialLayout, 0); - info.initialKeyguardLayout = sa.getResourceId(com.android.internal.R.styleable. - AppWidgetProviderInfo_initialKeyguardLayout, 0); - String className = sa - .getString(com.android.internal.R.styleable.AppWidgetProviderInfo_configure); - if (className != null) { - info.configure = new ComponentName(component.getPackageName(), className); - } - info.label = activityInfo.loadLabel(mContext.getPackageManager()).toString(); - info.icon = ri.getIconResource(); - info.previewImage = sa.getResourceId( - com.android.internal.R.styleable.AppWidgetProviderInfo_previewImage, 0); - info.autoAdvanceViewId = sa.getResourceId( - com.android.internal.R.styleable.AppWidgetProviderInfo_autoAdvanceViewId, -1); - info.resizeMode = sa.getInt( - com.android.internal.R.styleable.AppWidgetProviderInfo_resizeMode, - AppWidgetProviderInfo.RESIZE_NONE); - info.widgetCategory = sa.getInt( - com.android.internal.R.styleable.AppWidgetProviderInfo_widgetCategory, - AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN); - - sa.recycle(); - } catch (Exception e) { - // Ok to catch Exception here, because anything going wrong because - // of what a client process passes to us should not be fatal for the - // system process. - Slog.w(TAG, "XML parsing failed for AppWidget provider '" + component + '\'', e); - return null; - } finally { - if (parser != null) - parser.close(); - } - return p; - } - - int getUidForPackage(String packageName) throws PackageManager.NameNotFoundException { - PackageInfo pkgInfo = null; - try { - pkgInfo = mPm.getPackageInfo(packageName, 0, mUserId); - } catch (RemoteException re) { - // Shouldn't happen, local call - } - if (pkgInfo == null || pkgInfo.applicationInfo == null) { - throw new PackageManager.NameNotFoundException(); - } - return pkgInfo.applicationInfo.uid; - } - - int enforceSystemOrCallingUid(String packageName) throws IllegalArgumentException { - int callingUid = Binder.getCallingUid(); - if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID || callingUid == 0) { - return callingUid; - } - return enforceCallingUid(packageName); - } - - int enforceCallingUid(String packageName) throws IllegalArgumentException { - int callingUid = Binder.getCallingUid(); - int packageUid; - try { - packageUid = getUidForPackage(packageName); - } catch (PackageManager.NameNotFoundException ex) { - throw new IllegalArgumentException("packageName and uid don't match packageName=" - + packageName); - } - if (!UserHandle.isSameApp(callingUid, packageUid)) { - throw new IllegalArgumentException("packageName and uid don't match packageName=" - + packageName); - } - return callingUid; - } - - void sendInitialBroadcasts() { - synchronized (mAppWidgetIds) { - ensureStateLoadedLocked(); - final int N = mInstalledProviders.size(); - for (int i = 0; i < N; i++) { - Provider p = mInstalledProviders.get(i); - if (p.instances.size() > 0) { - sendEnableIntentLocked(p); - int[] appWidgetIds = getAppWidgetIds(p); - sendUpdateIntentLocked(p, appWidgetIds); - registerForBroadcastsLocked(p, appWidgetIds); - } - } - } - } - - // only call from initialization -- it assumes that the data structures are all empty - void loadStateLocked() { - AtomicFile file = savedStateFile(); - try { - FileInputStream stream = file.openRead(); - readStateFromFileLocked(stream); - - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - Slog.w(TAG, "Failed to close state FileInputStream " + e); - } - } - } catch (FileNotFoundException e) { - Slog.w(TAG, "Failed to read state: " + e); - } - } - - void saveStateLocked() { - if (!mHasFeature) { - return; - } - AtomicFile file = savedStateFile(); - FileOutputStream stream; - try { - stream = file.startWrite(); - if (writeStateToFileLocked(stream)) { - file.finishWrite(stream); - } else { - file.failWrite(stream); - Slog.w(TAG, "Failed to save state, restoring backup."); - } - } catch (IOException e) { - Slog.w(TAG, "Failed open state file for write: " + e); - } - } - - boolean writeStateToFileLocked(FileOutputStream stream) { - int N; - - try { - XmlSerializer out = new FastXmlSerializer(); - out.setOutput(stream, "utf-8"); - out.startDocument(null, true); - out.startTag(null, "gs"); - out.attribute(null, "version", String.valueOf(CURRENT_VERSION)); - int providerIndex = 0; - N = mInstalledProviders.size(); - for (int i = 0; i < N; i++) { - Provider p = mInstalledProviders.get(i); - if (p.instances.size() > 0) { - out.startTag(null, "p"); - out.attribute(null, "pkg", p.info.provider.getPackageName()); - out.attribute(null, "cl", p.info.provider.getClassName()); - out.endTag(null, "p"); - p.tag = providerIndex; - providerIndex++; - } - } - - N = mHosts.size(); - for (int i = 0; i < N; i++) { - Host host = mHosts.get(i); - out.startTag(null, "h"); - out.attribute(null, "pkg", host.packageName); - out.attribute(null, "id", Integer.toHexString(host.hostId)); - out.endTag(null, "h"); - host.tag = i; - } - - N = mAppWidgetIds.size(); - for (int i = 0; i < N; i++) { - AppWidgetId id = mAppWidgetIds.get(i); - out.startTag(null, "g"); - out.attribute(null, "id", Integer.toHexString(id.appWidgetId)); - out.attribute(null, "h", Integer.toHexString(id.host.tag)); - if (id.provider != null) { - out.attribute(null, "p", Integer.toHexString(id.provider.tag)); - } - if (id.options != null) { - out.attribute(null, "min_width", Integer.toHexString(id.options.getInt( - AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH))); - out.attribute(null, "min_height", Integer.toHexString(id.options.getInt( - AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT))); - out.attribute(null, "max_width", Integer.toHexString(id.options.getInt( - AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH))); - out.attribute(null, "max_height", Integer.toHexString(id.options.getInt( - AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT))); - out.attribute(null, "host_category", Integer.toHexString(id.options.getInt( - AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY))); - } - out.endTag(null, "g"); - } - - Iterator it = mPackagesWithBindWidgetPermission.iterator(); - while (it.hasNext()) { - out.startTag(null, "b"); - out.attribute(null, "packageName", it.next()); - out.endTag(null, "b"); - } - - out.endTag(null, "gs"); - - out.endDocument(); - return true; - } catch (IOException e) { - Slog.w(TAG, "Failed to write state: " + e); - return false; - } - } - - @SuppressWarnings("unused") - void readStateFromFileLocked(FileInputStream stream) { - boolean success = false; - int version = 0; - try { - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(stream, null); - - int type; - int providerIndex = 0; - HashMap loadedProviders = new HashMap(); - do { - type = parser.next(); - if (type == XmlPullParser.START_TAG) { - String tag = parser.getName(); - if ("gs".equals(tag)) { - String attributeValue = parser.getAttributeValue(null, "version"); - try { - version = Integer.parseInt(attributeValue); - } catch (NumberFormatException e) { - version = 0; - } - } else if ("p".equals(tag)) { - // TODO: do we need to check that this package has the same signature - // as before? - String pkg = parser.getAttributeValue(null, "pkg"); - String cl = parser.getAttributeValue(null, "cl"); - - final IPackageManager packageManager = AppGlobals.getPackageManager(); - try { - packageManager.getReceiverInfo(new ComponentName(pkg, cl), 0, mUserId); - } catch (RemoteException e) { - String[] pkgs = mContext.getPackageManager() - .currentToCanonicalPackageNames(new String[] { pkg }); - pkg = pkgs[0]; - } - - Provider p = lookupProviderLocked(new ComponentName(pkg, cl)); - if (p == null && mSafeMode) { - // if we're in safe mode, make a temporary one - p = new Provider(); - p.info = new AppWidgetProviderInfo(); - p.info.provider = new ComponentName(pkg, cl); - p.zombie = true; - mInstalledProviders.add(p); - } - if (p != null) { - // if it wasn't uninstalled or something - loadedProviders.put(providerIndex, p); - } - providerIndex++; - } else if ("h".equals(tag)) { - Host host = new Host(); - - // TODO: do we need to check that this package has the same signature - // as before? - host.packageName = parser.getAttributeValue(null, "pkg"); - try { - host.uid = getUidForPackage(host.packageName); - } catch (PackageManager.NameNotFoundException ex) { - host.zombie = true; - } - if (!host.zombie || mSafeMode) { - // In safe mode, we don't discard the hosts we don't recognize - // so that they're not pruned from our list. Otherwise, we do. - host.hostId = Integer - .parseInt(parser.getAttributeValue(null, "id"), 16); - mHosts.add(host); - } - } else if ("b".equals(tag)) { - String packageName = parser.getAttributeValue(null, "packageName"); - if (packageName != null) { - mPackagesWithBindWidgetPermission.add(packageName); - } - } else if ("g".equals(tag)) { - AppWidgetId id = new AppWidgetId(); - id.appWidgetId = Integer.parseInt(parser.getAttributeValue(null, "id"), 16); - if (id.appWidgetId >= mNextAppWidgetId) { - mNextAppWidgetId = id.appWidgetId + 1; - } - - Bundle options = new Bundle(); - String minWidthString = parser.getAttributeValue(null, "min_width"); - if (minWidthString != null) { - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, - Integer.parseInt(minWidthString, 16)); - } - String minHeightString = parser.getAttributeValue(null, "min_height"); - if (minHeightString != null) { - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, - Integer.parseInt(minHeightString, 16)); - } - String maxWidthString = parser.getAttributeValue(null, "max_width"); - if (maxWidthString != null) { - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - Integer.parseInt(maxWidthString, 16)); - } - String maxHeightString = parser.getAttributeValue(null, "max_height"); - if (maxHeightString != null) { - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, - Integer.parseInt(maxHeightString, 16)); - } - String categoryString = parser.getAttributeValue(null, "host_category"); - if (categoryString != null) { - options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, - Integer.parseInt(categoryString, 16)); - } - id.options = options; - - String providerString = parser.getAttributeValue(null, "p"); - if (providerString != null) { - // there's no provider if it hasn't been bound yet. - // maybe we don't have to save this, but it brings the system - // to the state it was in. - int pIndex = Integer.parseInt(providerString, 16); - id.provider = loadedProviders.get(pIndex); - if (false) { - Slog.d(TAG, "bound appWidgetId=" + id.appWidgetId + " to provider " - + pIndex + " which is " + id.provider); - } - if (id.provider == null) { - // This provider is gone. We just let the host figure out - // that this happened when it fails to load it. - continue; - } - } - - int hIndex = Integer.parseInt(parser.getAttributeValue(null, "h"), 16); - id.host = mHosts.get(hIndex); - if (id.host == null) { - // This host is gone. - continue; - } - - if (id.provider != null) { - id.provider.instances.add(id); - } - id.host.instances.add(id); - mAppWidgetIds.add(id); - } - } - } while (type != XmlPullParser.END_DOCUMENT); - success = true; - } catch (NullPointerException e) { - Slog.w(TAG, "failed parsing " + e); - } catch (NumberFormatException e) { - Slog.w(TAG, "failed parsing " + e); - } catch (XmlPullParserException e) { - Slog.w(TAG, "failed parsing " + e); - } catch (IOException e) { - Slog.w(TAG, "failed parsing " + e); - } catch (IndexOutOfBoundsException e) { - Slog.w(TAG, "failed parsing " + e); - } - - if (success) { - // delete any hosts that didn't manage to get connected (should happen) - // if it matters, they'll be reconnected. - for (int i = mHosts.size() - 1; i >= 0; i--) { - pruneHostLocked(mHosts.get(i)); - } - // upgrade the database if needed - performUpgrade(version); - } else { - // failed reading, clean up - Slog.w(TAG, "Failed to read state, clearing widgets and hosts."); - - mAppWidgetIds.clear(); - mHosts.clear(); - final int N = mInstalledProviders.size(); - for (int i = 0; i < N; i++) { - mInstalledProviders.get(i).instances.clear(); - } - } - } - - private void performUpgrade(int fromVersion) { - if (fromVersion < CURRENT_VERSION) { - Slog.v(TAG, "Upgrading widget database from " + fromVersion + " to " + CURRENT_VERSION - + " for user " + mUserId); - } - - int version = fromVersion; - - // Update 1: keyguard moved from package "android" to "com.android.keyguard" - if (version == 0) { - for (int i = 0; i < mHosts.size(); i++) { - Host host = mHosts.get(i); - if (host != null && "android".equals(host.packageName) - && host.hostId == KEYGUARD_HOST_ID) { - host.packageName = KEYGUARD_HOST_PACKAGE; - } - } - version = 1; - } - - if (version != CURRENT_VERSION) { - throw new IllegalStateException("Failed to upgrade widget database"); - } - } - - static File getSettingsFile(int userId) { - return new File(Environment.getUserSystemDirectory(userId), SETTINGS_FILENAME); - } - - AtomicFile savedStateFile() { - File dir = Environment.getUserSystemDirectory(mUserId); - File settingsFile = getSettingsFile(mUserId); - if (!settingsFile.exists() && mUserId == 0) { - if (!dir.exists()) { - dir.mkdirs(); - } - // Migrate old data - File oldFile = new File("/data/system/" + SETTINGS_FILENAME); - // Method doesn't throw an exception on failure. Ignore any errors - // in moving the file (like non-existence) - oldFile.renameTo(settingsFile); - } - return new AtomicFile(settingsFile); - } - - void onUserStopping() { - // prune the ones we don't want to keep - int N = mInstalledProviders.size(); - for (int i = N - 1; i >= 0; i--) { - Provider p = mInstalledProviders.get(i); - cancelBroadcasts(p); - } - } - - void onUserRemoved() { - getSettingsFile(mUserId).delete(); - } - - boolean addProvidersForPackageLocked(String pkgName) { - boolean providersAdded = false; - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - intent.setPackage(pkgName); - List broadcastReceivers; - try { - broadcastReceivers = mPm.queryIntentReceivers(intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - PackageManager.GET_META_DATA, mUserId); - } catch (RemoteException re) { - // Shouldn't happen, local call - return false; - } - final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); - for (int i = 0; i < N; i++) { - ResolveInfo ri = broadcastReceivers.get(i); - ActivityInfo ai = ri.activityInfo; - if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { - continue; - } - if (pkgName.equals(ai.packageName)) { - addProviderLocked(ri); - providersAdded = true; - } - } - - return providersAdded; - } - - /** - * Updates all providers with the specified package names, and records any providers that were - * pruned. - * - * @return whether any providers were updated - */ - boolean updateProvidersForPackageLocked(String pkgName, Set removedProviders) { - boolean providersUpdated = false; - HashSet keep = new HashSet(); - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - intent.setPackage(pkgName); - List broadcastReceivers; - try { - broadcastReceivers = mPm.queryIntentReceivers(intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - PackageManager.GET_META_DATA, mUserId); - } catch (RemoteException re) { - // Shouldn't happen, local call - return false; - } - - // add the missing ones and collect which ones to keep - int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); - for (int i = 0; i < N; i++) { - ResolveInfo ri = broadcastReceivers.get(i); - ActivityInfo ai = ri.activityInfo; - if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { - continue; - } - if (pkgName.equals(ai.packageName)) { - ComponentName component = new ComponentName(ai.packageName, ai.name); - Provider p = lookupProviderLocked(component); - if (p == null) { - if (addProviderLocked(ri)) { - keep.add(ai.name); - providersUpdated = true; - } - } else { - Provider parsed = parseProviderInfoXml(component, ri); - if (parsed != null) { - keep.add(ai.name); - // Use the new AppWidgetProviderInfo. - p.info = parsed.info; - // If it's enabled - final int M = p.instances.size(); - if (M > 0) { - int[] appWidgetIds = getAppWidgetIds(p); - // Reschedule for the new updatePeriodMillis (don't worry about handling - // it specially if updatePeriodMillis didn't change because we just sent - // an update, and the next one will be updatePeriodMillis from now). - cancelBroadcasts(p); - registerForBroadcastsLocked(p, appWidgetIds); - // If it's currently showing, call back with the new - // AppWidgetProviderInfo. - for (int j = 0; j < M; j++) { - AppWidgetId id = p.instances.get(j); - id.views = null; - if (id.host != null && id.host.callbacks != null) { - try { - id.host.callbacks.providerChanged(id.appWidgetId, p.info, - mUserId); - } catch (RemoteException ex) { - // It failed; remove the callback. No need to prune because - // we know that this host is still referenced by this - // instance. - id.host.callbacks = null; - } - } - } - // Now that we've told the host, push out an update. - sendUpdateIntentLocked(p, appWidgetIds); - providersUpdated = true; - } - } - } - } - } - - // prune the ones we don't want to keep - N = mInstalledProviders.size(); - for (int i = N - 1; i >= 0; i--) { - Provider p = mInstalledProviders.get(i); - if (pkgName.equals(p.info.provider.getPackageName()) - && !keep.contains(p.info.provider.getClassName())) { - if (removedProviders != null) { - removedProviders.add(p.info.provider); - } - removeProviderLocked(i, p); - providersUpdated = true; - } - } - - return providersUpdated; - } - - boolean removeProvidersForPackageLocked(String pkgName) { - boolean providersRemoved = false; - int N = mInstalledProviders.size(); - for (int i = N - 1; i >= 0; i--) { - Provider p = mInstalledProviders.get(i); - if (pkgName.equals(p.info.provider.getPackageName())) { - removeProviderLocked(i, p); - providersRemoved = true; - } - } - - // Delete the hosts for this package too - // - // By now, we have removed any AppWidgets that were in any hosts here, - // so we don't need to worry about sending DISABLE broadcasts to them. - N = mHosts.size(); - for (int i = N - 1; i >= 0; i--) { - Host host = mHosts.get(i); - if (pkgName.equals(host.packageName)) { - deleteHostLocked(host); - } - } - - return providersRemoved; - } - - void notifyHostsForProvidersChangedLocked() { - final int N = mHosts.size(); - for (int i = N - 1; i >= 0; i--) { - Host host = mHosts.get(i); - try { - if (host.callbacks != null) { - host.callbacks.providersChanged(mUserId); - } - } catch (RemoteException ex) { - // It failed; remove the callback. No need to prune because - // we know that this host is still referenced by this - // instance. - host.callbacks = null; - } - } - } -} diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java deleted file mode 100644 index 00bfee78bfbb..000000000000 --- a/services/java/com/android/server/BackupManagerService.java +++ /dev/null @@ -1,6193 +0,0 @@ -/* - * 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 com.android.server; - -import android.app.ActivityManagerNative; -import android.app.AlarmManager; -import android.app.AppGlobals; -import android.app.IActivityManager; -import android.app.IApplicationThread; -import android.app.IBackupAgent; -import android.app.PendingIntent; -import android.app.backup.BackupAgent; -import android.app.backup.BackupDataOutput; -import android.app.backup.FullBackup; -import android.app.backup.RestoreSet; -import android.app.backup.IBackupManager; -import android.app.backup.IFullBackupRestoreObserver; -import android.app.backup.IRestoreObserver; -import android.app.backup.IRestoreSession; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentResolver; -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.IPackageDeleteObserver; -import android.content.pm.IPackageInstallObserver; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.Signature; -import android.content.pm.PackageManager.NameNotFoundException; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.os.PowerManager; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.WorkSource; -import android.os.Environment.UserEnvironment; -import android.os.storage.IMountService; -import android.provider.Settings; -import android.util.EventLog; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; -import android.util.StringBuilderPrinter; - -import com.android.internal.backup.BackupConstants; -import com.android.internal.backup.IBackupTransport; -import com.android.internal.backup.IObbBackupService; -import com.android.internal.backup.LocalTransport; -import com.android.server.PackageManagerBackupAgent.Metadata; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.EOFException; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.RandomAccessFile; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -class BackupManagerService extends IBackupManager.Stub { - private static final String TAG = "BackupManagerService"; - private static final boolean DEBUG = true; - private static final boolean MORE_DEBUG = false; - - // Name and current contents version of the full-backup manifest file - static final String BACKUP_MANIFEST_FILENAME = "_manifest"; - static final int BACKUP_MANIFEST_VERSION = 1; - static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; - static final int BACKUP_FILE_VERSION = 1; - static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production - - static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; - static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; - - // How often we perform a backup pass. Privileged external callers can - // trigger an immediate pass. - private static final long BACKUP_INTERVAL = AlarmManager.INTERVAL_HOUR; - - // Random variation in backup scheduling time to avoid server load spikes - private static final int FUZZ_MILLIS = 5 * 60 * 1000; - - // The amount of time between the initial provisioning of the device and - // the first backup pass. - private static final long FIRST_BACKUP_INTERVAL = 12 * AlarmManager.INTERVAL_HOUR; - - // Retry interval for clear/init when the transport is unavailable - private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR; - - private static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN"; - private static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT"; - private static final String RUN_CLEAR_ACTION = "android.app.backup.intent.CLEAR"; - private static final int MSG_RUN_BACKUP = 1; - private static final int MSG_RUN_FULL_BACKUP = 2; - private static final int MSG_RUN_RESTORE = 3; - private static final int MSG_RUN_CLEAR = 4; - private static final int MSG_RUN_INITIALIZE = 5; - private static final int MSG_RUN_GET_RESTORE_SETS = 6; - private static final int MSG_TIMEOUT = 7; - private static final int MSG_RESTORE_TIMEOUT = 8; - private static final int MSG_FULL_CONFIRMATION_TIMEOUT = 9; - private static final int MSG_RUN_FULL_RESTORE = 10; - private static final int MSG_RETRY_INIT = 11; - private static final int MSG_RETRY_CLEAR = 12; - - // backup task state machine tick - static final int MSG_BACKUP_RESTORE_STEP = 20; - static final int MSG_OP_COMPLETE = 21; - - // Timeout interval for deciding that a bind or clear-data has taken too long - static final long TIMEOUT_INTERVAL = 10 * 1000; - - // Timeout intervals for agent backup & restore operations - static final long TIMEOUT_BACKUP_INTERVAL = 30 * 1000; - static final long TIMEOUT_FULL_BACKUP_INTERVAL = 5 * 60 * 1000; - static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000; - static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000; - - // User confirmation timeout for a full backup/restore operation. It's this long in - // order to give them time to enter the backup password. - static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000; - - private Context mContext; - private PackageManager mPackageManager; - IPackageManager mPackageManagerBinder; - private IActivityManager mActivityManager; - private PowerManager mPowerManager; - private AlarmManager mAlarmManager; - private IMountService mMountService; - IBackupManager mBackupManagerBinder; - - boolean mEnabled; // access to this is synchronized on 'this' - boolean mProvisioned; - boolean mAutoRestore; - PowerManager.WakeLock mWakelock; - HandlerThread mHandlerThread; - BackupHandler mBackupHandler; - PendingIntent mRunBackupIntent, mRunInitIntent; - BroadcastReceiver mRunBackupReceiver, mRunInitReceiver; - // map UIDs to the set of participating packages under that UID - final SparseArray> mBackupParticipants - = new SparseArray>(); - // set of backup services that have pending changes - class BackupRequest { - public String packageName; - - BackupRequest(String pkgName) { - packageName = pkgName; - } - - public String toString() { - return "BackupRequest{pkg=" + packageName + "}"; - } - } - // Backups that we haven't started yet. Keys are package names. - HashMap mPendingBackups - = new HashMap(); - - // Pseudoname that we use for the Package Manager metadata "package" - static final String PACKAGE_MANAGER_SENTINEL = "@pm@"; - - // locking around the pending-backup management - final Object mQueueLock = new Object(); - - // The thread performing the sequence of queued backups binds to each app's agent - // in succession. Bind notifications are asynchronously delivered through the - // Activity Manager; use this lock object to signal when a requested binding has - // completed. - final Object mAgentConnectLock = new Object(); - IBackupAgent mConnectedAgent; - volatile boolean mBackupRunning; - volatile boolean mConnecting; - volatile long mLastBackupPass; - volatile long mNextBackupPass; - - // For debugging, we maintain a progress trace of operations during backup - static final boolean DEBUG_BACKUP_TRACE = true; - final List mBackupTrace = new ArrayList(); - - // A similar synchronization mechanism around clearing apps' data for restore - final Object mClearDataLock = new Object(); - volatile boolean mClearingData; - - // Transport bookkeeping - final HashMap mTransportNames - = new HashMap(); // component name -> registration name - final HashMap mTransports - = new HashMap(); // registration name -> binder - final ArrayList mTransportConnections - = new ArrayList(); - String mCurrentTransport; - ActiveRestoreSession mActiveRestoreSession; - - // Watch the device provisioning operation during setup - ContentObserver mProvisionedObserver; - - class ProvisionedObserver extends ContentObserver { - public ProvisionedObserver(Handler handler) { - super(handler); - } - - public void onChange(boolean selfChange) { - final boolean wasProvisioned = mProvisioned; - final boolean isProvisioned = deviceIsProvisioned(); - // latch: never unprovision - mProvisioned = wasProvisioned || isProvisioned; - if (MORE_DEBUG) { - Slog.d(TAG, "Provisioning change: was=" + wasProvisioned - + " is=" + isProvisioned + " now=" + mProvisioned); - } - - synchronized (mQueueLock) { - if (mProvisioned && !wasProvisioned && mEnabled) { - // we're now good to go, so start the backup alarms - if (MORE_DEBUG) Slog.d(TAG, "Now provisioned, so starting backups"); - startBackupAlarmsLocked(FIRST_BACKUP_INTERVAL); - } - } - } - } - - class RestoreGetSetsParams { - public IBackupTransport transport; - public ActiveRestoreSession session; - public IRestoreObserver observer; - - RestoreGetSetsParams(IBackupTransport _transport, ActiveRestoreSession _session, - IRestoreObserver _observer) { - transport = _transport; - session = _session; - observer = _observer; - } - } - - class RestoreParams { - public IBackupTransport transport; - public String dirName; - public IRestoreObserver observer; - public long token; - public PackageInfo pkgInfo; - public int pmToken; // in post-install restore, the PM's token for this transaction - public boolean needFullBackup; - public String[] filterSet; - - RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, - long _token, PackageInfo _pkg, int _pmToken, boolean _needFullBackup) { - transport = _transport; - dirName = _dirName; - observer = _obs; - token = _token; - pkgInfo = _pkg; - pmToken = _pmToken; - needFullBackup = _needFullBackup; - filterSet = null; - } - - RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, - long _token, boolean _needFullBackup) { - transport = _transport; - dirName = _dirName; - observer = _obs; - token = _token; - pkgInfo = null; - pmToken = 0; - needFullBackup = _needFullBackup; - filterSet = null; - } - - RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, - long _token, String[] _filterSet, boolean _needFullBackup) { - transport = _transport; - dirName = _dirName; - observer = _obs; - token = _token; - pkgInfo = null; - pmToken = 0; - needFullBackup = _needFullBackup; - filterSet = _filterSet; - } - } - - class ClearParams { - public IBackupTransport transport; - public PackageInfo packageInfo; - - ClearParams(IBackupTransport _transport, PackageInfo _info) { - transport = _transport; - packageInfo = _info; - } - } - - class ClearRetryParams { - public String transportName; - public String packageName; - - ClearRetryParams(String transport, String pkg) { - transportName = transport; - packageName = pkg; - } - } - - class FullParams { - public ParcelFileDescriptor fd; - public final AtomicBoolean latch; - public IFullBackupRestoreObserver observer; - public String curPassword; // filled in by the confirmation step - public String encryptPassword; - - FullParams() { - latch = new AtomicBoolean(false); - } - } - - class FullBackupParams extends FullParams { - public boolean includeApks; - public boolean includeObbs; - public boolean includeShared; - public boolean allApps; - public boolean includeSystem; - public String[] packages; - - FullBackupParams(ParcelFileDescriptor output, boolean saveApks, boolean saveObbs, - boolean saveShared, boolean doAllApps, boolean doSystem, String[] pkgList) { - fd = output; - includeApks = saveApks; - includeObbs = saveObbs; - includeShared = saveShared; - allApps = doAllApps; - includeSystem = doSystem; - packages = pkgList; - } - } - - class FullRestoreParams extends FullParams { - FullRestoreParams(ParcelFileDescriptor input) { - fd = input; - } - } - - // Bookkeeping of in-flight operations for timeout etc. purposes. The operation - // token is the index of the entry in the pending-operations list. - static final int OP_PENDING = 0; - static final int OP_ACKNOWLEDGED = 1; - static final int OP_TIMEOUT = -1; - - class Operation { - public int state; - public BackupRestoreTask callback; - - Operation(int initialState, BackupRestoreTask callbackObj) { - state = initialState; - callback = callbackObj; - } - } - final SparseArray mCurrentOperations = new SparseArray(); - final Object mCurrentOpLock = new Object(); - final Random mTokenGenerator = new Random(); - - final SparseArray mFullConfirmations = new SparseArray(); - - // Where we keep our journal files and other bookkeeping - File mBaseStateDir; - File mDataDir; - File mJournalDir; - File mJournal; - - // Backup password, if any, and the file where it's saved. What is stored is not the - // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but - // persisted) salt. Validation is performed by running the challenge text through the - // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches - // the saved hash string, then the challenge text matches the originally supplied - // password text. - private final SecureRandom mRng = new SecureRandom(); - private String mPasswordHash; - private File mPasswordHashFile; - private byte[] mPasswordSalt; - - // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys - static final int PBKDF2_HASH_ROUNDS = 10000; - static final int PBKDF2_KEY_SIZE = 256; // bits - static final int PBKDF2_SALT_SIZE = 512; // bits - static final String ENCRYPTION_ALGORITHM_NAME = "AES-256"; - - // Keep a log of all the apps we've ever backed up, and what the - // dataset tokens are for both the current backup dataset and - // the ancestral dataset. - private File mEverStored; - HashSet mEverStoredApps = new HashSet(); - - static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1; // increment when the schema changes - File mTokenFile; - Set mAncestralPackages = null; - long mAncestralToken = 0; - long mCurrentToken = 0; - - // Persistently track the need to do a full init - static final String INIT_SENTINEL_FILE_NAME = "_need_init_"; - HashSet mPendingInits = new HashSet(); // transport names - - // Utility: build a new random integer token - int generateToken() { - int token; - do { - synchronized (mTokenGenerator) { - token = mTokenGenerator.nextInt(); - } - } while (token < 0); - return token; - } - - // ----- Asynchronous backup/restore handler thread ----- - - private class BackupHandler extends Handler { - public BackupHandler(Looper looper) { - super(looper); - } - - public void handleMessage(Message msg) { - - switch (msg.what) { - case MSG_RUN_BACKUP: - { - mLastBackupPass = System.currentTimeMillis(); - mNextBackupPass = mLastBackupPass + BACKUP_INTERVAL; - - IBackupTransport transport = getTransport(mCurrentTransport); - if (transport == null) { - Slog.v(TAG, "Backup requested but no transport available"); - synchronized (mQueueLock) { - mBackupRunning = false; - } - mWakelock.release(); - break; - } - - // snapshot the pending-backup set and work on that - ArrayList queue = new ArrayList(); - File oldJournal = mJournal; - synchronized (mQueueLock) { - // Do we have any work to do? Construct the work queue - // then release the synchronization lock to actually run - // the backup. - if (mPendingBackups.size() > 0) { - for (BackupRequest b: mPendingBackups.values()) { - queue.add(b); - } - if (DEBUG) Slog.v(TAG, "clearing pending backups"); - mPendingBackups.clear(); - - // Start a new backup-queue journal file too - mJournal = null; - - } - } - - // At this point, we have started a new journal file, and the old - // file identity is being passed to the backup processing task. - // When it completes successfully, that old journal file will be - // deleted. If we crash prior to that, the old journal is parsed - // at next boot and the journaled requests fulfilled. - boolean staged = true; - if (queue.size() > 0) { - // Spin up a backup state sequence and set it running - try { - String dirName = transport.transportDirName(); - PerformBackupTask pbt = new PerformBackupTask(transport, dirName, - queue, oldJournal); - Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt); - sendMessage(pbtMessage); - } catch (RemoteException e) { - // unable to ask the transport its dir name -- transient failure, since - // the above check succeeded. Try again next time. - Slog.e(TAG, "Transport became unavailable attempting backup"); - staged = false; - } - } else { - Slog.v(TAG, "Backup requested but nothing pending"); - staged = false; - } - - if (!staged) { - // if we didn't actually hand off the wakelock, rewind until next time - synchronized (mQueueLock) { - mBackupRunning = false; - } - mWakelock.release(); - } - break; - } - - case MSG_BACKUP_RESTORE_STEP: - { - try { - BackupRestoreTask task = (BackupRestoreTask) msg.obj; - if (MORE_DEBUG) Slog.v(TAG, "Got next step for " + task + ", executing"); - task.execute(); - } catch (ClassCastException e) { - Slog.e(TAG, "Invalid backup task in flight, obj=" + msg.obj); - } - break; - } - - case MSG_OP_COMPLETE: - { - try { - BackupRestoreTask task = (BackupRestoreTask) msg.obj; - task.operationComplete(); - } catch (ClassCastException e) { - Slog.e(TAG, "Invalid completion in flight, obj=" + msg.obj); - } - break; - } - - case MSG_RUN_FULL_BACKUP: - { - // TODO: refactor full backup to be a looper-based state machine - // similar to normal backup/restore. - FullBackupParams params = (FullBackupParams)msg.obj; - PerformFullBackupTask task = new PerformFullBackupTask(params.fd, - params.observer, params.includeApks, params.includeObbs, - params.includeShared, params.curPassword, params.encryptPassword, - params.allApps, params.includeSystem, params.packages, params.latch); - (new Thread(task)).start(); - break; - } - - case MSG_RUN_RESTORE: - { - RestoreParams params = (RestoreParams)msg.obj; - Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer); - PerformRestoreTask task = new PerformRestoreTask( - params.transport, params.dirName, params.observer, - params.token, params.pkgInfo, params.pmToken, - params.needFullBackup, params.filterSet); - Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task); - sendMessage(restoreMsg); - break; - } - - case MSG_RUN_FULL_RESTORE: - { - // TODO: refactor full restore to be a looper-based state machine - // similar to normal backup/restore. - FullRestoreParams params = (FullRestoreParams)msg.obj; - PerformFullRestoreTask task = new PerformFullRestoreTask(params.fd, - params.curPassword, params.encryptPassword, - params.observer, params.latch); - (new Thread(task)).start(); - break; - } - - case MSG_RUN_CLEAR: - { - ClearParams params = (ClearParams)msg.obj; - (new PerformClearTask(params.transport, params.packageInfo)).run(); - break; - } - - case MSG_RETRY_CLEAR: - { - // reenqueues if the transport remains unavailable - ClearRetryParams params = (ClearRetryParams)msg.obj; - clearBackupData(params.transportName, params.packageName); - break; - } - - case MSG_RUN_INITIALIZE: - { - HashSet queue; - - // Snapshot the pending-init queue and work on that - synchronized (mQueueLock) { - queue = new HashSet(mPendingInits); - mPendingInits.clear(); - } - - (new PerformInitializeTask(queue)).run(); - break; - } - - case MSG_RETRY_INIT: - { - synchronized (mQueueLock) { - recordInitPendingLocked(msg.arg1 != 0, (String)msg.obj); - mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), - mRunInitIntent); - } - break; - } - - case MSG_RUN_GET_RESTORE_SETS: - { - // Like other async operations, this is entered with the wakelock held - RestoreSet[] sets = null; - RestoreGetSetsParams params = (RestoreGetSetsParams)msg.obj; - try { - sets = params.transport.getAvailableRestoreSets(); - // cache the result in the active session - synchronized (params.session) { - params.session.mRestoreSets = sets; - } - if (sets == null) EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - } catch (Exception e) { - Slog.e(TAG, "Error from transport getting set list"); - } finally { - if (params.observer != null) { - try { - params.observer.restoreSetsAvailable(sets); - } catch (RemoteException re) { - Slog.e(TAG, "Unable to report listing to observer"); - } catch (Exception e) { - Slog.e(TAG, "Restore observer threw", e); - } - } - - // Done: reset the session timeout clock - removeMessages(MSG_RESTORE_TIMEOUT); - sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); - - mWakelock.release(); - } - break; - } - - case MSG_TIMEOUT: - { - handleTimeout(msg.arg1, msg.obj); - break; - } - - case MSG_RESTORE_TIMEOUT: - { - synchronized (BackupManagerService.this) { - if (mActiveRestoreSession != null) { - // Client app left the restore session dangling. We know that it - // can't be in the middle of an actual restore operation because - // the timeout is suspended while a restore is in progress. Clean - // up now. - Slog.w(TAG, "Restore session timed out; aborting"); - post(mActiveRestoreSession.new EndRestoreRunnable( - BackupManagerService.this, mActiveRestoreSession)); - } - } - } - - case MSG_FULL_CONFIRMATION_TIMEOUT: - { - synchronized (mFullConfirmations) { - FullParams params = mFullConfirmations.get(msg.arg1); - if (params != null) { - Slog.i(TAG, "Full backup/restore timed out waiting for user confirmation"); - - // Release the waiter; timeout == completion - signalFullBackupRestoreCompletion(params); - - // Remove the token from the set - mFullConfirmations.delete(msg.arg1); - - // Report a timeout to the observer, if any - if (params.observer != null) { - try { - params.observer.onTimeout(); - } catch (RemoteException e) { - /* don't care if the app has gone away */ - } - } - } else { - Slog.d(TAG, "couldn't find params for token " + msg.arg1); - } - } - break; - } - } - } - } - - // ----- Debug-only backup operation trace ----- - void addBackupTrace(String s) { - if (DEBUG_BACKUP_TRACE) { - synchronized (mBackupTrace) { - mBackupTrace.add(s); - } - } - } - - void clearBackupTrace() { - if (DEBUG_BACKUP_TRACE) { - synchronized (mBackupTrace) { - mBackupTrace.clear(); - } - } - } - - // ----- Main service implementation ----- - - public BackupManagerService(Context context) { - mContext = context; - mPackageManager = context.getPackageManager(); - mPackageManagerBinder = AppGlobals.getPackageManager(); - mActivityManager = ActivityManagerNative.getDefault(); - - mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); - - mBackupManagerBinder = asInterface(asBinder()); - - // spin up the backup/restore handler thread - mHandlerThread = new HandlerThread("backup", Process.THREAD_PRIORITY_BACKGROUND); - mHandlerThread.start(); - mBackupHandler = new BackupHandler(mHandlerThread.getLooper()); - - // Set up our bookkeeping - final ContentResolver resolver = context.getContentResolver(); - boolean areEnabled = Settings.Secure.getInt(resolver, - Settings.Secure.BACKUP_ENABLED, 0) != 0; - mProvisioned = Settings.Global.getInt(resolver, - Settings.Global.DEVICE_PROVISIONED, 0) != 0; - mAutoRestore = Settings.Secure.getInt(resolver, - Settings.Secure.BACKUP_AUTO_RESTORE, 1) != 0; - - mProvisionedObserver = new ProvisionedObserver(mBackupHandler); - resolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), - false, mProvisionedObserver); - - // If Encrypted file systems is enabled or disabled, this call will return the - // correct directory. - mBaseStateDir = new File(Environment.getSecureDataDirectory(), "backup"); - mBaseStateDir.mkdirs(); - if (!SELinux.restorecon(mBaseStateDir)) { - Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir); - } - mDataDir = Environment.getDownloadCacheDirectory(); - - mPasswordHashFile = new File(mBaseStateDir, "pwhash"); - if (mPasswordHashFile.exists()) { - FileInputStream fin = null; - DataInputStream in = null; - try { - fin = new FileInputStream(mPasswordHashFile); - in = new DataInputStream(new BufferedInputStream(fin)); - // integer length of the salt array, followed by the salt, - // then the hex pw hash string - int saltLen = in.readInt(); - byte[] salt = new byte[saltLen]; - in.readFully(salt); - mPasswordHash = in.readUTF(); - mPasswordSalt = salt; - } catch (IOException e) { - Slog.e(TAG, "Unable to read saved backup pw hash"); - } finally { - try { - if (in != null) in.close(); - if (fin != null) fin.close(); - } catch (IOException e) { - Slog.w(TAG, "Unable to close streams"); - } - } - } - - // Alarm receivers for scheduled backups & initialization operations - mRunBackupReceiver = new RunBackupReceiver(); - IntentFilter filter = new IntentFilter(); - filter.addAction(RUN_BACKUP_ACTION); - context.registerReceiver(mRunBackupReceiver, filter, - android.Manifest.permission.BACKUP, null); - - mRunInitReceiver = new RunInitializeReceiver(); - filter = new IntentFilter(); - filter.addAction(RUN_INITIALIZE_ACTION); - context.registerReceiver(mRunInitReceiver, filter, - android.Manifest.permission.BACKUP, null); - - Intent backupIntent = new Intent(RUN_BACKUP_ACTION); - backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - mRunBackupIntent = PendingIntent.getBroadcast(context, MSG_RUN_BACKUP, backupIntent, 0); - - Intent initIntent = new Intent(RUN_INITIALIZE_ACTION); - backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - mRunInitIntent = PendingIntent.getBroadcast(context, MSG_RUN_INITIALIZE, initIntent, 0); - - // Set up the backup-request journaling - mJournalDir = new File(mBaseStateDir, "pending"); - mJournalDir.mkdirs(); // creates mBaseStateDir along the way - mJournal = null; // will be created on first use - - // Set up the various sorts of package tracking we do - initPackageTracking(); - - // Build our mapping of uid to backup client services. This implicitly - // schedules a backup pass on the Package Manager metadata the first - // time anything needs to be backed up. - synchronized (mBackupParticipants) { - addPackageParticipantsLocked(null); - } - - // Set up our transport options and initialize the default transport - // TODO: Don't create transports that we don't need to? - mCurrentTransport = Settings.Secure.getString(context.getContentResolver(), - Settings.Secure.BACKUP_TRANSPORT); - if ("".equals(mCurrentTransport)) { - mCurrentTransport = null; - } - if (DEBUG) Slog.v(TAG, "Starting with transport " + mCurrentTransport); - - // Find transport hosts and bind to their services - Intent transportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST); - List hosts = mPackageManager.queryIntentServicesAsUser( - transportServiceIntent, 0, UserHandle.USER_OWNER); - if (DEBUG) { - Slog.v(TAG, "Found transports: " + ((hosts == null) ? "null" : hosts.size())); - } - if (hosts != null) { - if (MORE_DEBUG) { - for (int i = 0; i < hosts.size(); i++) { - ServiceInfo info = hosts.get(i).serviceInfo; - Slog.v(TAG, " " + info.packageName + "/" + info.name); - } - } - for (int i = 0; i < hosts.size(); i++) { - try { - ServiceInfo info = hosts.get(i).serviceInfo; - PackageInfo packInfo = mPackageManager.getPackageInfo(info.packageName, 0); - if ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0) { - ComponentName svcName = new ComponentName(info.packageName, info.name); - if (DEBUG) { - Slog.i(TAG, "Binding to transport host " + svcName); - } - Intent intent = new Intent(transportServiceIntent); - intent.setComponent(svcName); - TransportConnection connection = new TransportConnection(); - mTransportConnections.add(connection); - context.bindServiceAsUser(intent, - connection, Context.BIND_AUTO_CREATE, - UserHandle.OWNER); - } else { - Slog.w(TAG, "Transport package not privileged: " + info.packageName); - } - } catch (Exception e) { - Slog.e(TAG, "Problem resolving transport service: " + e.getMessage()); - } - } - } - - // Now that we know about valid backup participants, parse any - // leftover journal files into the pending backup set - parseLeftoverJournals(); - - // Power management - mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"); - - // Start the backup passes going - setBackupEnabled(areEnabled); - } - - private class RunBackupReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - if (RUN_BACKUP_ACTION.equals(intent.getAction())) { - synchronized (mQueueLock) { - if (mPendingInits.size() > 0) { - // If there are pending init operations, we process those - // and then settle into the usual periodic backup schedule. - if (DEBUG) Slog.v(TAG, "Init pending at scheduled backup"); - try { - mAlarmManager.cancel(mRunInitIntent); - mRunInitIntent.send(); - } catch (PendingIntent.CanceledException ce) { - Slog.e(TAG, "Run init intent cancelled"); - // can't really do more than bail here - } - } else { - // Don't run backups now if we're disabled or not yet - // fully set up. - if (mEnabled && mProvisioned) { - if (!mBackupRunning) { - if (DEBUG) Slog.v(TAG, "Running a backup pass"); - - // Acquire the wakelock and pass it to the backup thread. it will - // be released once backup concludes. - mBackupRunning = true; - mWakelock.acquire(); - - Message msg = mBackupHandler.obtainMessage(MSG_RUN_BACKUP); - mBackupHandler.sendMessage(msg); - } else { - Slog.i(TAG, "Backup time but one already running"); - } - } else { - Slog.w(TAG, "Backup pass but e=" + mEnabled + " p=" + mProvisioned); - } - } - } - } - } - } - - private class RunInitializeReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - if (RUN_INITIALIZE_ACTION.equals(intent.getAction())) { - synchronized (mQueueLock) { - if (DEBUG) Slog.v(TAG, "Running a device init"); - - // Acquire the wakelock and pass it to the init thread. it will - // be released once init concludes. - mWakelock.acquire(); - - Message msg = mBackupHandler.obtainMessage(MSG_RUN_INITIALIZE); - mBackupHandler.sendMessage(msg); - } - } - } - } - - private void initPackageTracking() { - if (DEBUG) Slog.v(TAG, "Initializing package tracking"); - - // Remember our ancestral dataset - mTokenFile = new File(mBaseStateDir, "ancestral"); - try { - RandomAccessFile tf = new RandomAccessFile(mTokenFile, "r"); - int version = tf.readInt(); - if (version == CURRENT_ANCESTRAL_RECORD_VERSION) { - mAncestralToken = tf.readLong(); - mCurrentToken = tf.readLong(); - - int numPackages = tf.readInt(); - if (numPackages >= 0) { - mAncestralPackages = new HashSet(); - for (int i = 0; i < numPackages; i++) { - String pkgName = tf.readUTF(); - mAncestralPackages.add(pkgName); - } - } - } - tf.close(); - } catch (FileNotFoundException fnf) { - // Probably innocuous - Slog.v(TAG, "No ancestral data"); - } catch (IOException e) { - Slog.w(TAG, "Unable to read token file", e); - } - - // Keep a log of what apps we've ever backed up. Because we might have - // rebooted in the middle of an operation that was removing something from - // this log, we sanity-check its contents here and reconstruct it. - mEverStored = new File(mBaseStateDir, "processed"); - File tempProcessedFile = new File(mBaseStateDir, "processed.new"); - - // If we were in the middle of removing something from the ever-backed-up - // file, there might be a transient "processed.new" file still present. - // Ignore it -- we'll validate "processed" against the current package set. - if (tempProcessedFile.exists()) { - tempProcessedFile.delete(); - } - - // If there are previous contents, parse them out then start a new - // file to continue the recordkeeping. - if (mEverStored.exists()) { - RandomAccessFile temp = null; - RandomAccessFile in = null; - - try { - temp = new RandomAccessFile(tempProcessedFile, "rws"); - in = new RandomAccessFile(mEverStored, "r"); - - while (true) { - PackageInfo info; - String pkg = in.readUTF(); - try { - info = mPackageManager.getPackageInfo(pkg, 0); - mEverStoredApps.add(pkg); - temp.writeUTF(pkg); - if (MORE_DEBUG) Slog.v(TAG, " + " + pkg); - } catch (NameNotFoundException e) { - // nope, this package was uninstalled; don't include it - if (MORE_DEBUG) Slog.v(TAG, " - " + pkg); - } - } - } catch (EOFException e) { - // Once we've rewritten the backup history log, atomically replace the - // old one with the new one then reopen the file for continuing use. - if (!tempProcessedFile.renameTo(mEverStored)) { - Slog.e(TAG, "Error renaming " + tempProcessedFile + " to " + mEverStored); - } - } catch (IOException e) { - Slog.e(TAG, "Error in processed file", e); - } finally { - try { if (temp != null) temp.close(); } catch (IOException e) {} - try { if (in != null) in.close(); } catch (IOException e) {} - } - } - - // Register for broadcasts about package install, etc., so we can - // update the provider list. - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - mContext.registerReceiver(mBroadcastReceiver, filter); - // Register for events related to sdcard installation. - IntentFilter sdFilter = new IntentFilter(); - sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); - mContext.registerReceiver(mBroadcastReceiver, sdFilter); - } - - private void parseLeftoverJournals() { - for (File f : mJournalDir.listFiles()) { - if (mJournal == null || f.compareTo(mJournal) != 0) { - // This isn't the current journal, so it must be a leftover. Read - // out the package names mentioned there and schedule them for - // backup. - RandomAccessFile in = null; - try { - Slog.i(TAG, "Found stale backup journal, scheduling"); - in = new RandomAccessFile(f, "r"); - while (true) { - String packageName = in.readUTF(); - Slog.i(TAG, " " + packageName); - dataChangedImpl(packageName); - } - } catch (EOFException e) { - // no more data; we're done - } catch (Exception e) { - Slog.e(TAG, "Can't read " + f, e); - } finally { - // close/delete the file - try { if (in != null) in.close(); } catch (IOException e) {} - f.delete(); - } - } - } - } - - private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) { - return buildCharArrayKey(pw.toCharArray(), salt, rounds); - } - - private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) { - try { - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); - return keyFactory.generateSecret(ks); - } catch (InvalidKeySpecException e) { - Slog.e(TAG, "Invalid key spec for PBKDF2!"); - } catch (NoSuchAlgorithmException e) { - Slog.e(TAG, "PBKDF2 unavailable!"); - } - return null; - } - - private String buildPasswordHash(String pw, byte[] salt, int rounds) { - SecretKey key = buildPasswordKey(pw, salt, rounds); - if (key != null) { - return byteArrayToHex(key.getEncoded()); - } - return null; - } - - private String byteArrayToHex(byte[] data) { - StringBuilder buf = new StringBuilder(data.length * 2); - for (int i = 0; i < data.length; i++) { - buf.append(Byte.toHexString(data[i], true)); - } - return buf.toString(); - } - - private byte[] hexToByteArray(String digits) { - final int bytes = digits.length() / 2; - if (2*bytes != digits.length()) { - throw new IllegalArgumentException("Hex string must have an even number of digits"); - } - - byte[] result = new byte[bytes]; - for (int i = 0; i < digits.length(); i += 2) { - result[i/2] = (byte) Integer.parseInt(digits.substring(i, i+2), 16); - } - return result; - } - - private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { - char[] mkAsChar = new char[pwBytes.length]; - for (int i = 0; i < pwBytes.length; i++) { - mkAsChar[i] = (char) pwBytes[i]; - } - - Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); - return checksum.getEncoded(); - } - - // Used for generating random salts or passwords - private byte[] randomBytes(int bits) { - byte[] array = new byte[bits / 8]; - mRng.nextBytes(array); - return array; - } - - // Backup password management - boolean passwordMatchesSaved(String candidatePw, int rounds) { - // First, on an encrypted device we require matching the device pw - final boolean isEncrypted; - try { - isEncrypted = (mMountService.getEncryptionState() != MountService.ENCRYPTION_STATE_NONE); - if (isEncrypted) { - if (DEBUG) { - Slog.i(TAG, "Device encrypted; verifying against device data pw"); - } - // 0 means the password validated - // -2 means device not encrypted - // Any other result is either password failure or an error condition, - // so we refuse the match - final int result = mMountService.verifyEncryptionPassword(candidatePw); - if (result == 0) { - if (MORE_DEBUG) Slog.d(TAG, "Pw verifies"); - return true; - } else if (result != -2) { - if (MORE_DEBUG) Slog.d(TAG, "Pw mismatch"); - return false; - } else { - // ...else the device is supposedly not encrypted. HOWEVER, the - // query about the encryption state said that the device *is* - // encrypted, so ... we may have a problem. Log it and refuse - // the backup. - Slog.e(TAG, "verified encryption state mismatch against query; no match allowed"); - return false; - } - } - } catch (Exception e) { - // Something went wrong talking to the mount service. This is very bad; - // assume that we fail password validation. - return false; - } - - if (mPasswordHash == null) { - // no current password case -- require that 'currentPw' be null or empty - if (candidatePw == null || "".equals(candidatePw)) { - return true; - } // else the non-empty candidate does not match the empty stored pw - } else { - // hash the stated current pw and compare to the stored one - if (candidatePw != null && candidatePw.length() > 0) { - String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds); - if (mPasswordHash.equalsIgnoreCase(currentPwHash)) { - // candidate hash matches the stored hash -- the password matches - return true; - } - } // else the stored pw is nonempty but the candidate is empty; no match - } - return false; - } - - @Override - public boolean setBackupPassword(String currentPw, String newPw) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "setBackupPassword"); - - // If the supplied pw doesn't hash to the the saved one, fail - if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) { - return false; - } - - // Clearing the password is okay - if (newPw == null || newPw.isEmpty()) { - if (mPasswordHashFile.exists()) { - if (!mPasswordHashFile.delete()) { - // Unable to delete the old pw file, so fail - Slog.e(TAG, "Unable to clear backup password"); - return false; - } - } - mPasswordHash = null; - mPasswordSalt = null; - return true; - } - - try { - // Okay, build the hash of the new backup password - byte[] salt = randomBytes(PBKDF2_SALT_SIZE); - String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS); - - OutputStream pwf = null, buffer = null; - DataOutputStream out = null; - try { - pwf = new FileOutputStream(mPasswordHashFile); - buffer = new BufferedOutputStream(pwf); - out = new DataOutputStream(buffer); - // integer length of the salt array, followed by the salt, - // then the hex pw hash string - out.writeInt(salt.length); - out.write(salt); - out.writeUTF(newPwHash); - out.flush(); - mPasswordHash = newPwHash; - mPasswordSalt = salt; - return true; - } finally { - if (out != null) out.close(); - if (buffer != null) buffer.close(); - if (pwf != null) pwf.close(); - } - } catch (IOException e) { - Slog.e(TAG, "Unable to set backup password"); - } - return false; - } - - @Override - public boolean hasBackupPassword() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "hasBackupPassword"); - - try { - return (mMountService.getEncryptionState() != IMountService.ENCRYPTION_STATE_NONE) - || (mPasswordHash != null && mPasswordHash.length() > 0); - } catch (Exception e) { - // If we can't talk to the mount service we have a serious problem; fail - // "secure" i.e. assuming that we require a password - return true; - } - } - - // Maintain persistent state around whether need to do an initialize operation. - // Must be called with the queue lock held. - void recordInitPendingLocked(boolean isPending, String transportName) { - if (DEBUG) Slog.i(TAG, "recordInitPendingLocked: " + isPending - + " on transport " + transportName); - mBackupHandler.removeMessages(MSG_RETRY_INIT); - - try { - IBackupTransport transport = getTransport(transportName); - if (transport != null) { - String transportDirName = transport.transportDirName(); - File stateDir = new File(mBaseStateDir, transportDirName); - File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME); - - if (isPending) { - // We need an init before we can proceed with sending backup data. - // Record that with an entry in our set of pending inits, as well as - // journaling it via creation of a sentinel file. - mPendingInits.add(transportName); - try { - (new FileOutputStream(initPendingFile)).close(); - } catch (IOException ioe) { - // Something is badly wrong with our permissions; just try to move on - } - } else { - // No more initialization needed; wipe the journal and reset our state. - initPendingFile.delete(); - mPendingInits.remove(transportName); - } - return; // done; don't fall through to the error case - } - } catch (RemoteException e) { - // transport threw when asked its name; fall through to the lookup-failed case - } - - // The named transport doesn't exist or threw. This operation is - // important, so we record the need for a an init and post a message - // to retry the init later. - if (isPending) { - mPendingInits.add(transportName); - mBackupHandler.sendMessageDelayed( - mBackupHandler.obtainMessage(MSG_RETRY_INIT, - (isPending ? 1 : 0), - 0, - transportName), - TRANSPORT_RETRY_INTERVAL); - } - } - - // Reset all of our bookkeeping, in response to having been told that - // the backend data has been wiped [due to idle expiry, for example], - // so we must re-upload all saved settings. - void resetBackupState(File stateFileDir) { - synchronized (mQueueLock) { - // Wipe the "what we've ever backed up" tracking - mEverStoredApps.clear(); - mEverStored.delete(); - - mCurrentToken = 0; - writeRestoreTokens(); - - // Remove all the state files - for (File sf : stateFileDir.listFiles()) { - // ... but don't touch the needs-init sentinel - if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) { - sf.delete(); - } - } - } - - // Enqueue a new backup of every participant - synchronized (mBackupParticipants) { - final int N = mBackupParticipants.size(); - for (int i=0; i participants = mBackupParticipants.valueAt(i); - if (participants != null) { - for (String packageName : participants) { - dataChangedImpl(packageName); - } - } - } - } - } - - // Add a transport to our set of available backends. If 'transport' is null, this - // is an unregistration, and the transport's entry is removed from our bookkeeping. - private void registerTransport(String name, String component, IBackupTransport transport) { - synchronized (mTransports) { - if (DEBUG) Slog.v(TAG, "Registering transport " - + component + "::" + name + " = " + transport); - if (transport != null) { - mTransports.put(name, transport); - mTransportNames.put(component, name); - } else { - mTransports.remove(mTransportNames.get(component)); - mTransportNames.remove(component); - // Nothing further to do in the unregistration case - return; - } - } - - // If the init sentinel file exists, we need to be sure to perform the init - // as soon as practical. We also create the state directory at registration - // time to ensure it's present from the outset. - try { - String transportName = transport.transportDirName(); - File stateDir = new File(mBaseStateDir, transportName); - stateDir.mkdirs(); - - File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME); - if (initSentinel.exists()) { - synchronized (mQueueLock) { - mPendingInits.add(transportName); - - // TODO: pick a better starting time than now + 1 minute - long delay = 1000 * 60; // one minute, in milliseconds - mAlarmManager.set(AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + delay, mRunInitIntent); - } - } - } catch (RemoteException e) { - // the transport threw when asked its file naming prefs; declare it invalid - Slog.e(TAG, "Unable to register transport as " + name); - mTransportNames.remove(component); - mTransports.remove(name); - } - } - - // ----- Track installation/removal of packages ----- - BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - public void onReceive(Context context, Intent intent) { - if (DEBUG) Slog.d(TAG, "Received broadcast " + intent); - - String action = intent.getAction(); - boolean replacing = false; - boolean added = false; - Bundle extras = intent.getExtras(); - String pkgList[] = null; - if (Intent.ACTION_PACKAGE_ADDED.equals(action) || - Intent.ACTION_PACKAGE_REMOVED.equals(action)) { - Uri uri = intent.getData(); - if (uri == null) { - return; - } - String pkgName = uri.getSchemeSpecificPart(); - if (pkgName != null) { - pkgList = new String[] { pkgName }; - } - added = Intent.ACTION_PACKAGE_ADDED.equals(action); - replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false); - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { - added = true; - pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { - added = false; - pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - } - - if (pkgList == null || pkgList.length == 0) { - return; - } - - final int uid = extras.getInt(Intent.EXTRA_UID); - if (added) { - synchronized (mBackupParticipants) { - if (replacing) { - // This is the package-replaced case; we just remove the entry - // under the old uid and fall through to re-add. - removePackageParticipantsLocked(pkgList, uid); - } - addPackageParticipantsLocked(pkgList); - } - } else { - if (replacing) { - // The package is being updated. We'll receive a PACKAGE_ADDED shortly. - } else { - synchronized (mBackupParticipants) { - removePackageParticipantsLocked(pkgList, uid); - } - } - } - } - }; - - // ----- Track connection to transports service ----- - class TransportConnection implements ServiceConnection { - @Override - public void onServiceConnected(ComponentName component, IBinder service) { - if (DEBUG) Slog.v(TAG, "Connected to transport " + component); - try { - IBackupTransport transport = IBackupTransport.Stub.asInterface(service); - registerTransport(transport.name(), component.flattenToShortString(), transport); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to register transport " + component); - } - } - - @Override - public void onServiceDisconnected(ComponentName component) { - if (DEBUG) Slog.v(TAG, "Disconnected from transport " + component); - registerTransport(null, component.flattenToShortString(), null); - } - }; - - // Add the backup agents in the given packages to our set of known backup participants. - // If 'packageNames' is null, adds all backup agents in the whole system. - void addPackageParticipantsLocked(String[] packageNames) { - // Look for apps that define the android:backupAgent attribute - List targetApps = allAgentPackages(); - if (packageNames != null) { - if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: #" + packageNames.length); - for (String packageName : packageNames) { - addPackageParticipantsLockedInner(packageName, targetApps); - } - } else { - if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: all"); - addPackageParticipantsLockedInner(null, targetApps); - } - } - - private void addPackageParticipantsLockedInner(String packageName, - List targetPkgs) { - if (MORE_DEBUG) { - Slog.v(TAG, "Examining " + packageName + " for backup agent"); - } - - for (PackageInfo pkg : targetPkgs) { - if (packageName == null || pkg.packageName.equals(packageName)) { - int uid = pkg.applicationInfo.uid; - HashSet set = mBackupParticipants.get(uid); - if (set == null) { - set = new HashSet(); - mBackupParticipants.put(uid, set); - } - set.add(pkg.packageName); - if (MORE_DEBUG) Slog.v(TAG, "Agent found; added"); - - // Schedule a backup for it on general principles - if (DEBUG) Slog.i(TAG, "Scheduling backup for new app " + pkg.packageName); - dataChangedImpl(pkg.packageName); - } - } - } - - // Remove the given packages' entries from our known active set. - void removePackageParticipantsLocked(String[] packageNames, int oldUid) { - if (packageNames == null) { - Slog.w(TAG, "removePackageParticipants with null list"); - return; - } - - if (DEBUG) Slog.v(TAG, "removePackageParticipantsLocked: uid=" + oldUid - + " #" + packageNames.length); - for (String pkg : packageNames) { - // Known previous UID, so we know which package set to check - HashSet set = mBackupParticipants.get(oldUid); - if (set != null && set.contains(pkg)) { - removePackageFromSetLocked(set, pkg); - if (set.isEmpty()) { - if (MORE_DEBUG) Slog.v(TAG, " last one of this uid; purging set"); - mBackupParticipants.remove(oldUid); - } - } - } - } - - private void removePackageFromSetLocked(final HashSet set, - final String packageName) { - if (set.contains(packageName)) { - // Found it. Remove this one package from the bookkeeping, and - // if it's the last participating app under this uid we drop the - // (now-empty) set as well. - // Note that we deliberately leave it 'known' in the "ever backed up" - // bookkeeping so that its current-dataset data will be retrieved - // if the app is subsequently reinstalled - if (MORE_DEBUG) Slog.v(TAG, " removing participant " + packageName); - set.remove(packageName); - mPendingBackups.remove(packageName); - } - } - - // Returns the set of all applications that define an android:backupAgent attribute - List allAgentPackages() { - // !!! TODO: cache this and regenerate only when necessary - int flags = PackageManager.GET_SIGNATURES; - List packages = mPackageManager.getInstalledPackages(flags); - int N = packages.size(); - for (int a = N-1; a >= 0; a--) { - PackageInfo pkg = packages.get(a); - try { - ApplicationInfo app = pkg.applicationInfo; - if (((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) - || app.backupAgentName == null) { - packages.remove(a); - } - else { - // we will need the shared library path, so look that up and store it here - app = mPackageManager.getApplicationInfo(pkg.packageName, - PackageManager.GET_SHARED_LIBRARY_FILES); - pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles; - } - } catch (NameNotFoundException e) { - packages.remove(a); - } - } - return packages; - } - - // Called from the backup task: record that the given app has been successfully - // backed up at least once - void logBackupComplete(String packageName) { - if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return; - - synchronized (mEverStoredApps) { - if (!mEverStoredApps.add(packageName)) return; - - RandomAccessFile out = null; - try { - out = new RandomAccessFile(mEverStored, "rws"); - out.seek(out.length()); - out.writeUTF(packageName); - } catch (IOException e) { - Slog.e(TAG, "Can't log backup of " + packageName + " to " + mEverStored); - } finally { - try { if (out != null) out.close(); } catch (IOException e) {} - } - } - } - - // Remove our awareness of having ever backed up the given package - void removeEverBackedUp(String packageName) { - if (DEBUG) Slog.v(TAG, "Removing backed-up knowledge of " + packageName); - if (MORE_DEBUG) Slog.v(TAG, "New set:"); - - synchronized (mEverStoredApps) { - // Rewrite the file and rename to overwrite. If we reboot in the middle, - // we'll recognize on initialization time that the package no longer - // exists and fix it up then. - File tempKnownFile = new File(mBaseStateDir, "processed.new"); - RandomAccessFile known = null; - try { - known = new RandomAccessFile(tempKnownFile, "rws"); - mEverStoredApps.remove(packageName); - for (String s : mEverStoredApps) { - known.writeUTF(s); - if (MORE_DEBUG) Slog.v(TAG, " " + s); - } - known.close(); - known = null; - if (!tempKnownFile.renameTo(mEverStored)) { - throw new IOException("Can't rename " + tempKnownFile + " to " + mEverStored); - } - } catch (IOException e) { - // Bad: we couldn't create the new copy. For safety's sake we - // abandon the whole process and remove all what's-backed-up - // state entirely, meaning we'll force a backup pass for every - // participant on the next boot or [re]install. - Slog.w(TAG, "Error rewriting " + mEverStored, e); - mEverStoredApps.clear(); - tempKnownFile.delete(); - mEverStored.delete(); - } finally { - try { if (known != null) known.close(); } catch (IOException e) {} - } - } - } - - // Persistently record the current and ancestral backup tokens as well - // as the set of packages with data [supposedly] available in the - // ancestral dataset. - void writeRestoreTokens() { - try { - RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd"); - - // First, the version number of this record, for futureproofing - af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION); - - // Write the ancestral and current tokens - af.writeLong(mAncestralToken); - af.writeLong(mCurrentToken); - - // Now write the set of ancestral packages - if (mAncestralPackages == null) { - af.writeInt(-1); - } else { - af.writeInt(mAncestralPackages.size()); - if (DEBUG) Slog.v(TAG, "Ancestral packages: " + mAncestralPackages.size()); - for (String pkgName : mAncestralPackages) { - af.writeUTF(pkgName); - if (MORE_DEBUG) Slog.v(TAG, " " + pkgName); - } - } - af.close(); - } catch (IOException e) { - Slog.w(TAG, "Unable to write token file:", e); - } - } - - // Return the given transport - private IBackupTransport getTransport(String transportName) { - synchronized (mTransports) { - IBackupTransport transport = mTransports.get(transportName); - if (transport == null) { - Slog.w(TAG, "Requested unavailable transport: " + transportName); - } - return transport; - } - } - - // fire off a backup agent, blocking until it attaches or times out - IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { - IBackupAgent agent = null; - synchronized(mAgentConnectLock) { - mConnecting = true; - mConnectedAgent = null; - try { - if (mActivityManager.bindBackupAgent(app, mode)) { - Slog.d(TAG, "awaiting agent for " + app); - - // success; wait for the agent to arrive - // only wait 10 seconds for the bind to happen - long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; - while (mConnecting && mConnectedAgent == null - && (System.currentTimeMillis() < timeoutMark)) { - try { - mAgentConnectLock.wait(5000); - } catch (InterruptedException e) { - // just bail - if (DEBUG) Slog.w(TAG, "Interrupted: " + e); - mActivityManager.clearPendingBackup(); - return null; - } - } - - // if we timed out with no connect, abort and move on - if (mConnecting == true) { - Slog.w(TAG, "Timeout waiting for agent " + app); - mActivityManager.clearPendingBackup(); - return null; - } - if (DEBUG) Slog.i(TAG, "got agent " + mConnectedAgent); - agent = mConnectedAgent; - } - } catch (RemoteException e) { - // can't happen - ActivityManager is local - } - } - return agent; - } - - // clear an application's data, blocking until the operation completes or times out - void clearApplicationDataSynchronous(String packageName) { - // Don't wipe packages marked allowClearUserData=false - try { - PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); - if ((info.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) == 0) { - if (MORE_DEBUG) Slog.i(TAG, "allowClearUserData=false so not wiping " - + packageName); - return; - } - } catch (NameNotFoundException e) { - Slog.w(TAG, "Tried to clear data for " + packageName + " but not found"); - return; - } - - ClearDataObserver observer = new ClearDataObserver(); - - synchronized(mClearDataLock) { - mClearingData = true; - try { - mActivityManager.clearApplicationUserData(packageName, observer, 0); - } catch (RemoteException e) { - // can't happen because the activity manager is in this process - } - - // only wait 10 seconds for the clear data to happen - long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; - while (mClearingData && (System.currentTimeMillis() < timeoutMark)) { - try { - mClearDataLock.wait(5000); - } catch (InterruptedException e) { - // won't happen, but still. - mClearingData = false; - } - } - } - } - - class ClearDataObserver extends IPackageDataObserver.Stub { - public void onRemoveCompleted(String packageName, boolean succeeded) { - synchronized(mClearDataLock) { - mClearingData = false; - mClearDataLock.notifyAll(); - } - } - } - - // Get the restore-set token for the best-available restore set for this package: - // the active set if possible, else the ancestral one. Returns zero if none available. - long getAvailableRestoreToken(String packageName) { - long token = mAncestralToken; - synchronized (mQueueLock) { - if (mEverStoredApps.contains(packageName)) { - token = mCurrentToken; - } - } - return token; - } - - // ----- - // Interface and methods used by the asynchronous-with-timeout backup/restore operations - - interface BackupRestoreTask { - // Execute one tick of whatever state machine the task implements - void execute(); - - // An operation that wanted a callback has completed - void operationComplete(); - - // An operation that wanted a callback has timed out - void handleTimeout(); - } - - void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback) { - if (MORE_DEBUG) Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token) - + " interval=" + interval); - synchronized (mCurrentOpLock) { - mCurrentOperations.put(token, new Operation(OP_PENDING, callback)); - - Message msg = mBackupHandler.obtainMessage(MSG_TIMEOUT, token, 0, callback); - mBackupHandler.sendMessageDelayed(msg, interval); - } - } - - // synchronous waiter case - boolean waitUntilOperationComplete(int token) { - if (MORE_DEBUG) Slog.i(TAG, "Blocking until operation complete for " - + Integer.toHexString(token)); - int finalState = OP_PENDING; - Operation op = null; - synchronized (mCurrentOpLock) { - while (true) { - op = mCurrentOperations.get(token); - if (op == null) { - // mysterious disappearance: treat as success with no callback - break; - } else { - if (op.state == OP_PENDING) { - try { - mCurrentOpLock.wait(); - } catch (InterruptedException e) {} - // When the wait is notified we loop around and recheck the current state - } else { - // No longer pending; we're done - finalState = op.state; - break; - } - } - } - } - - mBackupHandler.removeMessages(MSG_TIMEOUT); - if (MORE_DEBUG) Slog.v(TAG, "operation " + Integer.toHexString(token) - + " complete: finalState=" + finalState); - return finalState == OP_ACKNOWLEDGED; - } - - void handleTimeout(int token, Object obj) { - // Notify any synchronous waiters - Operation op = null; - synchronized (mCurrentOpLock) { - op = mCurrentOperations.get(token); - if (MORE_DEBUG) { - if (op == null) Slog.w(TAG, "Timeout of token " + Integer.toHexString(token) - + " but no op found"); - } - int state = (op != null) ? op.state : OP_TIMEOUT; - if (state == OP_PENDING) { - if (DEBUG) Slog.v(TAG, "TIMEOUT: token=" + Integer.toHexString(token)); - op.state = OP_TIMEOUT; - mCurrentOperations.put(token, op); - } - mCurrentOpLock.notifyAll(); - } - - // If there's a TimeoutHandler for this event, call it - if (op != null && op.callback != null) { - op.callback.handleTimeout(); - } - } - - // ----- Back up a set of applications via a worker thread ----- - - enum BackupState { - INITIAL, - RUNNING_QUEUE, - FINAL - } - - class PerformBackupTask implements BackupRestoreTask { - private static final String TAG = "PerformBackupTask"; - - IBackupTransport mTransport; - ArrayList mQueue; - ArrayList mOriginalQueue; - File mStateDir; - File mJournal; - BackupState mCurrentState; - - // carried information about the current in-flight operation - PackageInfo mCurrentPackage; - File mSavedStateName; - File mBackupDataName; - File mNewStateName; - ParcelFileDescriptor mSavedState; - ParcelFileDescriptor mBackupData; - ParcelFileDescriptor mNewState; - int mStatus; - boolean mFinished; - - public PerformBackupTask(IBackupTransport transport, String dirName, - ArrayList queue, File journal) { - mTransport = transport; - mOriginalQueue = queue; - mJournal = journal; - - mStateDir = new File(mBaseStateDir, dirName); - - mCurrentState = BackupState.INITIAL; - mFinished = false; - - addBackupTrace("STATE => INITIAL"); - } - - // Main entry point: perform one chunk of work, updating the state as appropriate - // and reposting the next chunk to the primary backup handler thread. - @Override - public void execute() { - switch (mCurrentState) { - case INITIAL: - beginBackup(); - break; - - case RUNNING_QUEUE: - invokeNextAgent(); - break; - - case FINAL: - if (!mFinished) finalizeBackup(); - else { - Slog.e(TAG, "Duplicate finish"); - } - mFinished = true; - break; - } - } - - // We're starting a backup pass. Initialize the transport and send - // the PM metadata blob if we haven't already. - void beginBackup() { - if (DEBUG_BACKUP_TRACE) { - clearBackupTrace(); - StringBuilder b = new StringBuilder(256); - b.append("beginBackup: ["); - for (BackupRequest req : mOriginalQueue) { - b.append(' '); - b.append(req.packageName); - } - b.append(" ]"); - addBackupTrace(b.toString()); - } - - mStatus = BackupConstants.TRANSPORT_OK; - - // Sanity check: if the queue is empty we have no work to do. - if (mOriginalQueue.isEmpty()) { - Slog.w(TAG, "Backup begun with an empty queue - nothing to do."); - addBackupTrace("queue empty at begin"); - executeNextState(BackupState.FINAL); - return; - } - - // We need to retain the original queue contents in case of transport - // failure, but we want a working copy that we can manipulate along - // the way. - mQueue = (ArrayList) mOriginalQueue.clone(); - - if (DEBUG) Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); - - File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); - try { - final String transportName = mTransport.transportDirName(); - EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); - - // If we haven't stored package manager metadata yet, we must init the transport. - if (mStatus == BackupConstants.TRANSPORT_OK && pmState.length() <= 0) { - Slog.i(TAG, "Initializing (wiping) backup state and transport storage"); - addBackupTrace("initializing transport " + transportName); - resetBackupState(mStateDir); // Just to make sure. - mStatus = mTransport.initializeDevice(); - - addBackupTrace("transport.initializeDevice() == " + mStatus); - if (mStatus == BackupConstants.TRANSPORT_OK) { - EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); - } else { - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); - Slog.e(TAG, "Transport error in initializeDevice()"); - } - } - - // The package manager doesn't have a proper etc, but since - // it's running here in the system process we can just set up its agent - // directly and use a synthetic BackupRequest. We always run this pass - // because it's cheap and this way we guarantee that we don't get out of - // step even if we're selecting among various transports at run time. - if (mStatus == BackupConstants.TRANSPORT_OK) { - PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent( - mPackageManager, allAgentPackages()); - mStatus = invokeAgentForBackup(PACKAGE_MANAGER_SENTINEL, - IBackupAgent.Stub.asInterface(pmAgent.onBind()), mTransport); - addBackupTrace("PMBA invoke: " + mStatus); - } - - if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { - // The backend reports that our dataset has been wiped. Note this in - // the event log; the no-success code below will reset the backup - // state as well. - EventLog.writeEvent(EventLogTags.BACKUP_RESET, mTransport.transportDirName()); - } - } catch (Exception e) { - Slog.e(TAG, "Error in backup thread", e); - addBackupTrace("Exception in backup thread: " + e); - mStatus = BackupConstants.TRANSPORT_ERROR; - } finally { - // If we've succeeded so far, invokeAgentForBackup() will have run the PM - // metadata and its completion/timeout callback will continue the state - // machine chain. If it failed that won't happen; we handle that now. - addBackupTrace("exiting prelim: " + mStatus); - if (mStatus != BackupConstants.TRANSPORT_OK) { - // if things went wrong at this point, we need to - // restage everything and try again later. - resetBackupState(mStateDir); // Just to make sure. - executeNextState(BackupState.FINAL); - } - } - } - - // Transport has been initialized and the PM metadata submitted successfully - // if that was warranted. Now we process the single next thing in the queue. - void invokeNextAgent() { - mStatus = BackupConstants.TRANSPORT_OK; - addBackupTrace("invoke q=" + mQueue.size()); - - // Sanity check that we have work to do. If not, skip to the end where - // we reestablish the wakelock invariants etc. - if (mQueue.isEmpty()) { - if (DEBUG) Slog.i(TAG, "queue now empty"); - executeNextState(BackupState.FINAL); - return; - } - - // pop the entry we're going to process on this step - BackupRequest request = mQueue.get(0); - mQueue.remove(0); - - Slog.d(TAG, "starting agent for backup of " + request); - addBackupTrace("launch agent for " + request.packageName); - - // Verify that the requested app exists; it might be something that - // requested a backup but was then uninstalled. The request was - // journalled and rather than tamper with the journal it's safer - // to sanity-check here. This also gives us the classname of the - // package's backup agent. - try { - mCurrentPackage = mPackageManager.getPackageInfo(request.packageName, - PackageManager.GET_SIGNATURES); - if (mCurrentPackage.applicationInfo.backupAgentName == null) { - // The manifest has changed but we had a stale backup request pending. - // This won't happen again because the app won't be requesting further - // backups. - Slog.i(TAG, "Package " + request.packageName - + " no longer supports backup; skipping"); - addBackupTrace("skipping - no agent, completion is noop"); - executeNextState(BackupState.RUNNING_QUEUE); - return; - } - - if ((mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { - // The app has been force-stopped or cleared or just installed, - // and not yet launched out of that state, so just as it won't - // receive broadcasts, we won't run it for backup. - addBackupTrace("skipping - stopped"); - executeNextState(BackupState.RUNNING_QUEUE); - return; - } - - IBackupAgent agent = null; - try { - mWakelock.setWorkSource(new WorkSource(mCurrentPackage.applicationInfo.uid)); - agent = bindToAgentSynchronous(mCurrentPackage.applicationInfo, - IApplicationThread.BACKUP_MODE_INCREMENTAL); - addBackupTrace("agent bound; a? = " + (agent != null)); - if (agent != null) { - mStatus = invokeAgentForBackup(request.packageName, agent, mTransport); - // at this point we'll either get a completion callback from the - // agent, or a timeout message on the main handler. either way, we're - // done here as long as we're successful so far. - } else { - // Timeout waiting for the agent - mStatus = BackupConstants.AGENT_ERROR; - } - } catch (SecurityException ex) { - // Try for the next one. - Slog.d(TAG, "error in bind/backup", ex); - mStatus = BackupConstants.AGENT_ERROR; - addBackupTrace("agent SE"); - } - } catch (NameNotFoundException e) { - Slog.d(TAG, "Package does not exist; skipping"); - addBackupTrace("no such package"); - mStatus = BackupConstants.AGENT_UNKNOWN; - } finally { - mWakelock.setWorkSource(null); - - // If there was an agent error, no timeout/completion handling will occur. - // That means we need to direct to the next state ourselves. - if (mStatus != BackupConstants.TRANSPORT_OK) { - BackupState nextState = BackupState.RUNNING_QUEUE; - - // An agent-level failure means we reenqueue this one agent for - // a later retry, but otherwise proceed normally. - if (mStatus == BackupConstants.AGENT_ERROR) { - if (MORE_DEBUG) Slog.i(TAG, "Agent failure for " + request.packageName - + " - restaging"); - dataChangedImpl(request.packageName); - mStatus = BackupConstants.TRANSPORT_OK; - if (mQueue.isEmpty()) nextState = BackupState.FINAL; - } else if (mStatus == BackupConstants.AGENT_UNKNOWN) { - // Failed lookup of the app, so we couldn't bring up an agent, but - // we're otherwise fine. Just drop it and go on to the next as usual. - mStatus = BackupConstants.TRANSPORT_OK; - } else { - // Transport-level failure means we reenqueue everything - revertAndEndBackup(); - nextState = BackupState.FINAL; - } - - executeNextState(nextState); - } else { - addBackupTrace("expecting completion/timeout callback"); - } - } - } - - void finalizeBackup() { - addBackupTrace("finishing"); - - // Either backup was successful, in which case we of course do not need - // this pass's journal any more; or it failed, in which case we just - // re-enqueued all of these packages in the current active journal. - // Either way, we no longer need this pass's journal. - if (mJournal != null && !mJournal.delete()) { - Slog.e(TAG, "Unable to remove backup journal file " + mJournal); - } - - // If everything actually went through and this is the first time we've - // done a backup, we can now record what the current backup dataset token - // is. - if ((mCurrentToken == 0) && (mStatus == BackupConstants.TRANSPORT_OK)) { - addBackupTrace("success; recording token"); - try { - mCurrentToken = mTransport.getCurrentRestoreSet(); - writeRestoreTokens(); - } catch (RemoteException e) { - // nothing for it at this point, unfortunately, but this will be - // recorded the next time we fully succeed. - addBackupTrace("transport threw returning token"); - } - } - - // Set up the next backup pass - at this point we can set mBackupRunning - // to false to allow another pass to fire, because we're done with the - // state machine sequence and the wakelock is refcounted. - synchronized (mQueueLock) { - mBackupRunning = false; - if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { - // Make sure we back up everything and perform the one-time init - clearMetadata(); - if (DEBUG) Slog.d(TAG, "Server requires init; rerunning"); - addBackupTrace("init required; rerunning"); - backupNow(); - } - } - - // Only once we're entirely finished do we release the wakelock - clearBackupTrace(); - Slog.i(TAG, "Backup pass finished."); - mWakelock.release(); - } - - // Remove the PM metadata state. This will generate an init on the next pass. - void clearMetadata() { - final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); - if (pmState.exists()) pmState.delete(); - } - - // Invoke an agent's doBackup() and start a timeout message spinning on the main - // handler in case it doesn't get back to us. - int invokeAgentForBackup(String packageName, IBackupAgent agent, - IBackupTransport transport) { - if (DEBUG) Slog.d(TAG, "invokeAgentForBackup on " + packageName); - addBackupTrace("invoking " + packageName); - - mSavedStateName = new File(mStateDir, packageName); - mBackupDataName = new File(mDataDir, packageName + ".data"); - mNewStateName = new File(mStateDir, packageName + ".new"); - - mSavedState = null; - mBackupData = null; - mNewState = null; - - final int token = generateToken(); - try { - // Look up the package info & signatures. This is first so that if it - // throws an exception, there's no file setup yet that would need to - // be unraveled. - if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) { - // The metadata 'package' is synthetic; construct one and make - // sure our global state is pointed at it - mCurrentPackage = new PackageInfo(); - mCurrentPackage.packageName = packageName; - } - - // In a full backup, we pass a null ParcelFileDescriptor as - // the saved-state "file". This is by definition an incremental, - // so we build a saved state file to pass. - mSavedState = ParcelFileDescriptor.open(mSavedStateName, - ParcelFileDescriptor.MODE_READ_ONLY | - ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary - - mBackupData = ParcelFileDescriptor.open(mBackupDataName, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - if (!SELinux.restorecon(mBackupDataName)) { - Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName); - } - - mNewState = ParcelFileDescriptor.open(mNewStateName, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - // Initiate the target's backup pass - addBackupTrace("setting timeout"); - prepareOperationTimeout(token, TIMEOUT_BACKUP_INTERVAL, this); - addBackupTrace("calling agent doBackup()"); - agent.doBackup(mSavedState, mBackupData, mNewState, token, mBackupManagerBinder); - } catch (Exception e) { - Slog.e(TAG, "Error invoking for backup on " + packageName); - addBackupTrace("exception: " + e); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, - e.toString()); - agentErrorCleanup(); - return BackupConstants.AGENT_ERROR; - } - - // At this point the agent is off and running. The next thing to happen will - // either be a callback from the agent, at which point we'll process its data - // for transport, or a timeout. Either way the next phase will happen in - // response to the TimeoutHandler interface callbacks. - addBackupTrace("invoke success"); - return BackupConstants.TRANSPORT_OK; - } - - @Override - public void operationComplete() { - // Okay, the agent successfully reported back to us. Spin the data off to the - // transport and proceed with the next stage. - if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for " - + mCurrentPackage.packageName); - mBackupHandler.removeMessages(MSG_TIMEOUT); - clearAgentState(); - addBackupTrace("operation complete"); - - ParcelFileDescriptor backupData = null; - mStatus = BackupConstants.TRANSPORT_OK; - try { - int size = (int) mBackupDataName.length(); - if (size > 0) { - if (mStatus == BackupConstants.TRANSPORT_OK) { - backupData = ParcelFileDescriptor.open(mBackupDataName, - ParcelFileDescriptor.MODE_READ_ONLY); - addBackupTrace("sending data to transport"); - mStatus = mTransport.performBackup(mCurrentPackage, backupData); - } - - // TODO - We call finishBackup() for each application backed up, because - // we need to know now whether it succeeded or failed. Instead, we should - // hold off on finishBackup() until the end, which implies holding off on - // renaming *all* the output state files (see below) until that happens. - - addBackupTrace("data delivered: " + mStatus); - if (mStatus == BackupConstants.TRANSPORT_OK) { - addBackupTrace("finishing op on transport"); - mStatus = mTransport.finishBackup(); - addBackupTrace("finished: " + mStatus); - } - } else { - if (DEBUG) Slog.i(TAG, "no backup data written; not calling transport"); - addBackupTrace("no data to send"); - } - - // After successful transport, delete the now-stale data - // and juggle the files so that next time we supply the agent - // with the new state file it just created. - if (mStatus == BackupConstants.TRANSPORT_OK) { - mBackupDataName.delete(); - mNewStateName.renameTo(mSavedStateName); - EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, - mCurrentPackage.packageName, size); - logBackupComplete(mCurrentPackage.packageName); - } else { - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, - mCurrentPackage.packageName); - } - } catch (Exception e) { - Slog.e(TAG, "Transport error backing up " + mCurrentPackage.packageName, e); - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, - mCurrentPackage.packageName); - mStatus = BackupConstants.TRANSPORT_ERROR; - } finally { - try { if (backupData != null) backupData.close(); } catch (IOException e) {} - } - - // If we encountered an error here it's a transport-level failure. That - // means we need to halt everything and reschedule everything for next time. - final BackupState nextState; - if (mStatus != BackupConstants.TRANSPORT_OK) { - revertAndEndBackup(); - nextState = BackupState.FINAL; - } else { - // Success! Proceed with the next app if any, otherwise we're done. - nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE; - } - - executeNextState(nextState); - } - - @Override - public void handleTimeout() { - // Whoops, the current agent timed out running doBackup(). Tidy up and restage - // it for the next time we run a backup pass. - // !!! TODO: keep track of failure counts per agent, and blacklist those which - // fail repeatedly (i.e. have proved themselves to be buggy). - Slog.e(TAG, "Timeout backing up " + mCurrentPackage.packageName); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, mCurrentPackage.packageName, - "timeout"); - addBackupTrace("timeout of " + mCurrentPackage.packageName); - agentErrorCleanup(); - dataChangedImpl(mCurrentPackage.packageName); - } - - void revertAndEndBackup() { - if (MORE_DEBUG) Slog.i(TAG, "Reverting backup queue - restaging everything"); - addBackupTrace("transport error; reverting"); - for (BackupRequest request : mOriginalQueue) { - dataChangedImpl(request.packageName); - } - // We also want to reset the backup schedule based on whatever - // the transport suggests by way of retry/backoff time. - restartBackupAlarm(); - } - - void agentErrorCleanup() { - mBackupDataName.delete(); - mNewStateName.delete(); - clearAgentState(); - - executeNextState(mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE); - } - - // Cleanup common to both success and failure cases - void clearAgentState() { - try { if (mSavedState != null) mSavedState.close(); } catch (IOException e) {} - try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} - try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} - mSavedState = mBackupData = mNewState = null; - synchronized (mCurrentOpLock) { - mCurrentOperations.clear(); - } - - // If this was a pseudopackage there's no associated Activity Manager state - if (mCurrentPackage.applicationInfo != null) { - addBackupTrace("unbinding " + mCurrentPackage.packageName); - try { // unbind even on timeout, just in case - mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); - } catch (RemoteException e) { /* can't happen; activity manager is local */ } - } - } - - void restartBackupAlarm() { - addBackupTrace("setting backup trigger"); - synchronized (mQueueLock) { - try { - startBackupAlarmsLocked(mTransport.requestBackupTime()); - } catch (RemoteException e) { /* cannot happen */ } - } - } - - void executeNextState(BackupState nextState) { - if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " - + this + " nextState=" + nextState); - addBackupTrace("executeNextState => " + nextState); - mCurrentState = nextState; - Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); - mBackupHandler.sendMessage(msg); - } - } - - - // ----- Full backup/restore to a file/socket ----- - - abstract class ObbServiceClient { - public IObbBackupService mObbService; - public void setObbBinder(IObbBackupService binder) { - mObbService = binder; - } - } - - class FullBackupObbConnection implements ServiceConnection { - volatile IObbBackupService mService; - - FullBackupObbConnection() { - mService = null; - } - - public void establish() { - if (DEBUG) Slog.i(TAG, "Initiating bind of OBB service on " + this); - Intent obbIntent = new Intent().setComponent(new ComponentName( - "com.android.sharedstoragebackup", - "com.android.sharedstoragebackup.ObbBackupService")); - BackupManagerService.this.mContext.bindService( - obbIntent, this, Context.BIND_AUTO_CREATE); - } - - public void tearDown() { - BackupManagerService.this.mContext.unbindService(this); - } - - public boolean backupObbs(PackageInfo pkg, OutputStream out) { - boolean success = false; - waitForConnection(); - - ParcelFileDescriptor[] pipes = null; - try { - pipes = ParcelFileDescriptor.createPipe(); - int token = generateToken(); - prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); - mService.backupObbs(pkg.packageName, pipes[1], token, mBackupManagerBinder); - routeSocketDataToOutput(pipes[0], out); - success = waitUntilOperationComplete(token); - } catch (Exception e) { - Slog.w(TAG, "Unable to back up OBBs for " + pkg, e); - } finally { - try { - out.flush(); - if (pipes != null) { - if (pipes[0] != null) pipes[0].close(); - if (pipes[1] != null) pipes[1].close(); - } - } catch (IOException e) { - Slog.w(TAG, "I/O error closing down OBB backup", e); - } - } - return success; - } - - public void restoreObbFile(String pkgName, ParcelFileDescriptor data, - long fileSize, int type, String path, long mode, long mtime, - int token, IBackupManager callbackBinder) { - waitForConnection(); - - try { - mService.restoreObbFile(pkgName, data, fileSize, type, path, mode, mtime, - token, callbackBinder); - } catch (Exception e) { - Slog.w(TAG, "Unable to restore OBBs for " + pkgName, e); - } - } - - private void waitForConnection() { - synchronized (this) { - while (mService == null) { - if (DEBUG) Slog.i(TAG, "...waiting for OBB service binding..."); - try { - this.wait(); - } catch (InterruptedException e) { /* never interrupted */ } - } - if (DEBUG) Slog.i(TAG, "Connected to OBB service; continuing"); - } - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - synchronized (this) { - mService = IObbBackupService.Stub.asInterface(service); - if (DEBUG) Slog.i(TAG, "OBB service connection " + mService - + " connected on " + this); - this.notifyAll(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - synchronized (this) { - mService = null; - if (DEBUG) Slog.i(TAG, "OBB service connection disconnected on " + this); - this.notifyAll(); - } - } - - } - - private void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out) - throws IOException { - FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor()); - DataInputStream in = new DataInputStream(raw); - - byte[] buffer = new byte[32 * 1024]; - int chunkTotal; - while ((chunkTotal = in.readInt()) > 0) { - while (chunkTotal > 0) { - int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal; - int nRead = in.read(buffer, 0, toRead); - out.write(buffer, 0, nRead); - chunkTotal -= nRead; - } - } - } - - class PerformFullBackupTask extends ObbServiceClient implements Runnable { - ParcelFileDescriptor mOutputFile; - DeflaterOutputStream mDeflater; - IFullBackupRestoreObserver mObserver; - boolean mIncludeApks; - boolean mIncludeObbs; - boolean mIncludeShared; - boolean mAllApps; - final boolean mIncludeSystem; - String[] mPackages; - String mCurrentPassword; - String mEncryptPassword; - AtomicBoolean mLatchObject; - File mFilesDir; - File mManifestFile; - - - class FullBackupRunner implements Runnable { - PackageInfo mPackage; - IBackupAgent mAgent; - ParcelFileDescriptor mPipe; - int mToken; - boolean mSendApk; - boolean mWriteManifest; - - FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe, - int token, boolean sendApk, boolean writeManifest) throws IOException { - mPackage = pack; - mAgent = agent; - mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor()); - mToken = token; - mSendApk = sendApk; - mWriteManifest = writeManifest; - } - - @Override - public void run() { - try { - BackupDataOutput output = new BackupDataOutput( - mPipe.getFileDescriptor()); - - if (mWriteManifest) { - if (MORE_DEBUG) Slog.d(TAG, "Writing manifest for " + mPackage.packageName); - writeAppManifest(mPackage, mManifestFile, mSendApk); - FullBackup.backupToTar(mPackage.packageName, null, null, - mFilesDir.getAbsolutePath(), - mManifestFile.getAbsolutePath(), - output); - } - - if (mSendApk) { - writeApkToBackup(mPackage, output); - } - - if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName); - prepareOperationTimeout(mToken, TIMEOUT_FULL_BACKUP_INTERVAL, null); - mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder); - } catch (IOException e) { - Slog.e(TAG, "Error running full backup for " + mPackage.packageName); - } catch (RemoteException e) { - Slog.e(TAG, "Remote agent vanished during full backup of " - + mPackage.packageName); - } finally { - try { - mPipe.close(); - } catch (IOException e) {} - } - } - } - - PerformFullBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer, - boolean includeApks, boolean includeObbs, boolean includeShared, - String curPassword, String encryptPassword, boolean doAllApps, - boolean doSystem, String[] packages, AtomicBoolean latch) { - mOutputFile = fd; - mObserver = observer; - mIncludeApks = includeApks; - mIncludeObbs = includeObbs; - mIncludeShared = includeShared; - mAllApps = doAllApps; - mIncludeSystem = doSystem; - mPackages = packages; - mCurrentPassword = curPassword; - // when backing up, if there is a current backup password, we require that - // the user use a nonempty encryption password as well. if one is supplied - // in the UI we use that, but if the UI was left empty we fall back to the - // current backup password (which was supplied by the user as well). - if (encryptPassword == null || "".equals(encryptPassword)) { - mEncryptPassword = curPassword; - } else { - mEncryptPassword = encryptPassword; - } - mLatchObject = latch; - - mFilesDir = new File("/data/system"); - mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME); - } - - @Override - public void run() { - Slog.i(TAG, "--- Performing full-dataset backup ---"); - - List packagesToBackup = new ArrayList(); - FullBackupObbConnection obbConnection = new FullBackupObbConnection(); - obbConnection.establish(); // we'll want this later - - sendStartBackup(); - - // doAllApps supersedes the package set if any - if (mAllApps) { - packagesToBackup = mPackageManager.getInstalledPackages( - PackageManager.GET_SIGNATURES); - // Exclude system apps if we've been asked to do so - if (mIncludeSystem == false) { - for (int i = 0; i < packagesToBackup.size(); ) { - PackageInfo pkg = packagesToBackup.get(i); - if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - packagesToBackup.remove(i); - } else { - i++; - } - } - } - } - - // Now process the command line argument packages, if any. Note that explicitly- - // named system-partition packages will be included even if includeSystem was - // set to false. - if (mPackages != null) { - for (String pkgName : mPackages) { - try { - packagesToBackup.add(mPackageManager.getPackageInfo(pkgName, - PackageManager.GET_SIGNATURES)); - } catch (NameNotFoundException e) { - Slog.w(TAG, "Unknown package " + pkgName + ", skipping"); - } - } - } - - // Cull any packages that have indicated that backups are not permitted, as well - // as any explicit mention of the 'special' shared-storage agent package (we - // handle that one at the end). - for (int i = 0; i < packagesToBackup.size(); ) { - PackageInfo pkg = packagesToBackup.get(i); - if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0 - || pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE)) { - packagesToBackup.remove(i); - } else { - i++; - } - } - - // Cull any packages that run as system-domain uids but do not define their - // own backup agents - for (int i = 0; i < packagesToBackup.size(); ) { - PackageInfo pkg = packagesToBackup.get(i); - if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) - && (pkg.applicationInfo.backupAgentName == null)) { - if (MORE_DEBUG) { - Slog.i(TAG, "... ignoring non-agent system package " + pkg.packageName); - } - packagesToBackup.remove(i); - } else { - i++; - } - } - - FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor()); - OutputStream out = null; - - PackageInfo pkg = null; - try { - boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0); - boolean compressing = COMPRESS_FULL_BACKUPS; - OutputStream finalOutput = ofstream; - - // Verify that the given password matches the currently-active - // backup password, if any - if (hasBackupPassword()) { - if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { - if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); - return; - } - } - - // Write the global file header. All strings are UTF-8 encoded; lines end - // with a '\n' byte. Actual backup data begins immediately following the - // final '\n'. - // - // line 1: "ANDROID BACKUP" - // line 2: backup file format version, currently "1" - // line 3: compressed? "0" if not compressed, "1" if compressed. - // line 4: name of encryption algorithm [currently only "none" or "AES-256"] - // - // When line 4 is not "none", then additional header data follows: - // - // line 5: user password salt [hex] - // line 6: master key checksum salt [hex] - // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal] - // line 8: IV of the user key [hex] - // line 9: master key blob [hex] - // IV of the master key, master key itself, master key checksum hash - // - // The master key checksum is the master key plus its checksum salt, run through - // 10k rounds of PBKDF2. This is used to verify that the user has supplied the - // correct password for decrypting the archive: the master key decrypted from - // the archive using the user-supplied password is also run through PBKDF2 in - // this way, and if the result does not match the checksum as stored in the - // archive, then we know that the user-supplied password does not match the - // archive's. - StringBuilder headerbuf = new StringBuilder(1024); - - headerbuf.append(BACKUP_FILE_HEADER_MAGIC); - headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n - headerbuf.append(compressing ? "\n1\n" : "\n0\n"); - - try { - // Set up the encryption stage if appropriate, and emit the correct header - if (encrypting) { - finalOutput = emitAesBackupHeader(headerbuf, finalOutput); - } else { - headerbuf.append("none\n"); - } - - byte[] header = headerbuf.toString().getBytes("UTF-8"); - ofstream.write(header); - - // Set up the compression stage feeding into the encryption stage (if any) - if (compressing) { - Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); - finalOutput = new DeflaterOutputStream(finalOutput, deflater, true); - } - - out = finalOutput; - } catch (Exception e) { - // Should never happen! - Slog.e(TAG, "Unable to emit archive header", e); - return; - } - - // Shared storage if requested - if (mIncludeShared) { - try { - pkg = mPackageManager.getPackageInfo(SHARED_BACKUP_AGENT_PACKAGE, 0); - packagesToBackup.add(pkg); - } catch (NameNotFoundException e) { - Slog.e(TAG, "Unable to find shared-storage backup handler"); - } - } - - // Now back up the app data via the agent mechanism - int N = packagesToBackup.size(); - for (int i = 0; i < N; i++) { - pkg = packagesToBackup.get(i); - backupOnePackage(pkg, out); - - // after the app's agent runs to handle its private filesystem - // contents, back up any OBB content it has on its behalf. - if (mIncludeObbs) { - boolean obbOkay = obbConnection.backupObbs(pkg, out); - if (!obbOkay) { - throw new RuntimeException("Failure writing OBB stack for " + pkg); - } - } - } - - // Done! - finalizeBackup(out); - } catch (RemoteException e) { - Slog.e(TAG, "App died during full backup"); - } catch (Exception e) { - Slog.e(TAG, "Internal exception during full backup", e); - } finally { - tearDown(pkg); - try { - if (out != null) out.close(); - mOutputFile.close(); - } catch (IOException e) { - /* nothing we can do about this */ - } - synchronized (mCurrentOpLock) { - mCurrentOperations.clear(); - } - synchronized (mLatchObject) { - mLatchObject.set(true); - mLatchObject.notifyAll(); - } - sendEndBackup(); - obbConnection.tearDown(); - if (DEBUG) Slog.d(TAG, "Full backup pass complete."); - mWakelock.release(); - } - } - - private OutputStream emitAesBackupHeader(StringBuilder headerbuf, - OutputStream ofstream) throws Exception { - // User key will be used to encrypt the master key. - byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); - SecretKey userKey = buildPasswordKey(mEncryptPassword, newUserSalt, - PBKDF2_HASH_ROUNDS); - - // the master key is random for each backup - byte[] masterPw = new byte[256 / 8]; - mRng.nextBytes(masterPw); - byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE); - - // primary encryption of the datastream with the random key - Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); - SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES"); - c.init(Cipher.ENCRYPT_MODE, masterKeySpec); - OutputStream finalOutput = new CipherOutputStream(ofstream, c); - - // line 4: name of encryption algorithm - headerbuf.append(ENCRYPTION_ALGORITHM_NAME); - headerbuf.append('\n'); - // line 5: user password salt [hex] - headerbuf.append(byteArrayToHex(newUserSalt)); - headerbuf.append('\n'); - // line 6: master key checksum salt [hex] - headerbuf.append(byteArrayToHex(checksumSalt)); - headerbuf.append('\n'); - // line 7: number of PBKDF2 rounds used [decimal] - headerbuf.append(PBKDF2_HASH_ROUNDS); - headerbuf.append('\n'); - - // line 8: IV of the user key [hex] - Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding"); - mkC.init(Cipher.ENCRYPT_MODE, userKey); - - byte[] IV = mkC.getIV(); - headerbuf.append(byteArrayToHex(IV)); - headerbuf.append('\n'); - - // line 9: master IV + key blob, encrypted by the user key [hex]. Blob format: - // [byte] IV length = Niv - // [array of Niv bytes] IV itself - // [byte] master key length = Nmk - // [array of Nmk bytes] master key itself - // [byte] MK checksum hash length = Nck - // [array of Nck bytes] master key checksum hash - // - // The checksum is the (master key + checksum salt), run through the - // stated number of PBKDF2 rounds - IV = c.getIV(); - byte[] mk = masterKeySpec.getEncoded(); - byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(), - checksumSalt, PBKDF2_HASH_ROUNDS); - - ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length - + checksum.length + 3); - DataOutputStream mkOut = new DataOutputStream(blob); - mkOut.writeByte(IV.length); - mkOut.write(IV); - mkOut.writeByte(mk.length); - mkOut.write(mk); - mkOut.writeByte(checksum.length); - mkOut.write(checksum); - mkOut.flush(); - byte[] encryptedMk = mkC.doFinal(blob.toByteArray()); - headerbuf.append(byteArrayToHex(encryptedMk)); - headerbuf.append('\n'); - - return finalOutput; - } - - private void backupOnePackage(PackageInfo pkg, OutputStream out) - throws RemoteException { - Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName); - - IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo, - IApplicationThread.BACKUP_MODE_FULL); - if (agent != null) { - ParcelFileDescriptor[] pipes = null; - try { - pipes = ParcelFileDescriptor.createPipe(); - - ApplicationInfo app = pkg.applicationInfo; - final boolean isSharedStorage = pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE); - final boolean sendApk = mIncludeApks - && !isSharedStorage - && ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) == 0) - && ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 || - (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); - - sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName); - - final int token = generateToken(); - FullBackupRunner runner = new FullBackupRunner(pkg, agent, pipes[1], - token, sendApk, !isSharedStorage); - pipes[1].close(); // the runner has dup'd it - pipes[1] = null; - Thread t = new Thread(runner); - t.start(); - - // Now pull data from the app and stuff it into the compressor - try { - routeSocketDataToOutput(pipes[0], out); - } catch (IOException e) { - Slog.i(TAG, "Caught exception reading from agent", e); - } - - if (!waitUntilOperationComplete(token)) { - Slog.e(TAG, "Full backup failed on package " + pkg.packageName); - } else { - if (DEBUG) Slog.d(TAG, "Full package backup success: " + pkg.packageName); - } - - } catch (IOException e) { - Slog.e(TAG, "Error backing up " + pkg.packageName, e); - } finally { - try { - // flush after every package - out.flush(); - if (pipes != null) { - if (pipes[0] != null) pipes[0].close(); - if (pipes[1] != null) pipes[1].close(); - } - } catch (IOException e) { - Slog.w(TAG, "Error bringing down backup stack"); - } - } - } else { - Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName); - } - tearDown(pkg); - } - - private void writeApkToBackup(PackageInfo pkg, BackupDataOutput output) { - // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here - final String appSourceDir = pkg.applicationInfo.sourceDir; - final String apkDir = new File(appSourceDir).getParent(); - FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null, - apkDir, appSourceDir, output); - - // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM - // doesn't have access to external storage. - - // Save associated .obb content if it exists and we did save the apk - // check for .obb and save those too - final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_OWNER); - final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0]; - if (obbDir != null) { - if (MORE_DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); - File[] obbFiles = obbDir.listFiles(); - if (obbFiles != null) { - final String obbDirName = obbDir.getAbsolutePath(); - for (File obb : obbFiles) { - FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null, - obbDirName, obb.getAbsolutePath(), output); - } - } - } - } - - private void finalizeBackup(OutputStream out) { - try { - // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes. - byte[] eof = new byte[512 * 2]; // newly allocated == zero filled - out.write(eof); - } catch (IOException e) { - Slog.w(TAG, "Error attempting to finalize backup stream"); - } - } - - private void writeAppManifest(PackageInfo pkg, File manifestFile, boolean withApk) - throws IOException { - // Manifest format. All data are strings ending in LF: - // BACKUP_MANIFEST_VERSION, currently 1 - // - // Version 1: - // package name - // package's versionCode - // platform versionCode - // getInstallerPackageName() for this package (maybe empty) - // boolean: "1" if archive includes .apk; any other string means not - // number of signatures == N - // N*: signature byte array in ascii format per Signature.toCharsString() - StringBuilder builder = new StringBuilder(4096); - StringBuilderPrinter printer = new StringBuilderPrinter(builder); - - printer.println(Integer.toString(BACKUP_MANIFEST_VERSION)); - printer.println(pkg.packageName); - printer.println(Integer.toString(pkg.versionCode)); - printer.println(Integer.toString(Build.VERSION.SDK_INT)); - - String installerName = mPackageManager.getInstallerPackageName(pkg.packageName); - printer.println((installerName != null) ? installerName : ""); - - printer.println(withApk ? "1" : "0"); - if (pkg.signatures == null) { - printer.println("0"); - } else { - printer.println(Integer.toString(pkg.signatures.length)); - for (Signature sig : pkg.signatures) { - printer.println(sig.toCharsString()); - } - } - - FileOutputStream outstream = new FileOutputStream(manifestFile); - outstream.write(builder.toString().getBytes()); - outstream.close(); - } - - private void tearDown(PackageInfo pkg) { - if (pkg != null) { - final ApplicationInfo app = pkg.applicationInfo; - if (app != null) { - try { - // unbind and tidy up even on timeout or failure, just in case - mActivityManager.unbindBackupAgent(app); - - // The agent was running with a stub Application object, so shut it down. - if (app.uid != Process.SYSTEM_UID - && app.uid != Process.PHONE_UID) { - if (MORE_DEBUG) Slog.d(TAG, "Backup complete, killing host process"); - mActivityManager.killApplicationProcess(app.processName, app.uid); - } else { - if (MORE_DEBUG) Slog.d(TAG, "Not killing after restore: " + app.processName); - } - } catch (RemoteException e) { - Slog.d(TAG, "Lost app trying to shut down"); - } - } - } - } - - // wrappers for observer use - void sendStartBackup() { - if (mObserver != null) { - try { - mObserver.onStartBackup(); - } catch (RemoteException e) { - Slog.w(TAG, "full backup observer went away: startBackup"); - mObserver = null; - } - } - } - - void sendOnBackupPackage(String name) { - if (mObserver != null) { - try { - // TODO: use a more user-friendly name string - mObserver.onBackupPackage(name); - } catch (RemoteException e) { - Slog.w(TAG, "full backup observer went away: backupPackage"); - mObserver = null; - } - } - } - - void sendEndBackup() { - if (mObserver != null) { - try { - mObserver.onEndBackup(); - } catch (RemoteException e) { - Slog.w(TAG, "full backup observer went away: endBackup"); - mObserver = null; - } - } - } - } - - - // ----- Full restore from a file/socket ----- - - // Description of a file in the restore datastream - static class FileMetadata { - String packageName; // name of the owning app - String installerPackageName; // name of the market-type app that installed the owner - int type; // e.g. BackupAgent.TYPE_DIRECTORY - String domain; // e.g. FullBackup.DATABASE_TREE_TOKEN - String path; // subpath within the semantic domain - long mode; // e.g. 0666 (actually int) - long mtime; // last mod time, UTC time_t (actually int) - long size; // bytes of content - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(128); - sb.append("FileMetadata{"); - sb.append(packageName); sb.append(','); - sb.append(type); sb.append(','); - sb.append(domain); sb.append(':'); sb.append(path); sb.append(','); - sb.append(size); - sb.append('}'); - return sb.toString(); - } - } - - enum RestorePolicy { - IGNORE, - ACCEPT, - ACCEPT_IF_APK - } - - class PerformFullRestoreTask extends ObbServiceClient implements Runnable { - ParcelFileDescriptor mInputFile; - String mCurrentPassword; - String mDecryptPassword; - IFullBackupRestoreObserver mObserver; - AtomicBoolean mLatchObject; - IBackupAgent mAgent; - String mAgentPackage; - ApplicationInfo mTargetApp; - FullBackupObbConnection mObbConnection = null; - ParcelFileDescriptor[] mPipes = null; - - long mBytes; - - // possible handling states for a given package in the restore dataset - final HashMap mPackagePolicies - = new HashMap(); - - // installer package names for each encountered app, derived from the manifests - final HashMap mPackageInstallers = new HashMap(); - - // Signatures for a given package found in its manifest file - final HashMap mManifestSignatures - = new HashMap(); - - // Packages we've already wiped data on when restoring their first file - final HashSet mClearedPackages = new HashSet(); - - PerformFullRestoreTask(ParcelFileDescriptor fd, String curPassword, String decryptPassword, - IFullBackupRestoreObserver observer, AtomicBoolean latch) { - mInputFile = fd; - mCurrentPassword = curPassword; - mDecryptPassword = decryptPassword; - mObserver = observer; - mLatchObject = latch; - mAgent = null; - mAgentPackage = null; - mTargetApp = null; - mObbConnection = new FullBackupObbConnection(); - - // Which packages we've already wiped data on. We prepopulate this - // with a whitelist of packages known to be unclearable. - mClearedPackages.add("android"); - mClearedPackages.add("com.android.providers.settings"); - - } - - class RestoreFileRunnable implements Runnable { - IBackupAgent mAgent; - FileMetadata mInfo; - ParcelFileDescriptor mSocket; - int mToken; - - RestoreFileRunnable(IBackupAgent agent, FileMetadata info, - ParcelFileDescriptor socket, int token) throws IOException { - mAgent = agent; - mInfo = info; - mToken = token; - - // This class is used strictly for process-local binder invocations. The - // semantics of ParcelFileDescriptor differ in this case; in particular, we - // do not automatically get a 'dup'ed descriptor that we can can continue - // to use asynchronously from the caller. So, we make sure to dup it ourselves - // before proceeding to do the restore. - mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); - } - - @Override - public void run() { - try { - mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type, - mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime, - mToken, mBackupManagerBinder); - } catch (RemoteException e) { - // never happens; this is used strictly for local binder calls - } - } - } - - @Override - public void run() { - Slog.i(TAG, "--- Performing full-dataset restore ---"); - mObbConnection.establish(); - sendStartRestore(); - - // Are we able to restore shared-storage data? - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - mPackagePolicies.put(SHARED_BACKUP_AGENT_PACKAGE, RestorePolicy.ACCEPT); - } - - FileInputStream rawInStream = null; - DataInputStream rawDataIn = null; - try { - if (hasBackupPassword()) { - if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { - if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); - return; - } - } - - mBytes = 0; - byte[] buffer = new byte[32 * 1024]; - rawInStream = new FileInputStream(mInputFile.getFileDescriptor()); - rawDataIn = new DataInputStream(rawInStream); - - // First, parse out the unencrypted/uncompressed header - boolean compressed = false; - InputStream preCompressStream = rawInStream; - final InputStream in; - - boolean okay = false; - final int headerLen = BACKUP_FILE_HEADER_MAGIC.length(); - byte[] streamHeader = new byte[headerLen]; - rawDataIn.readFully(streamHeader); - byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8"); - if (Arrays.equals(magicBytes, streamHeader)) { - // okay, header looks good. now parse out the rest of the fields. - String s = readHeaderLine(rawInStream); - if (Integer.parseInt(s) == BACKUP_FILE_VERSION) { - // okay, it's a version we recognize - s = readHeaderLine(rawInStream); - compressed = (Integer.parseInt(s) != 0); - s = readHeaderLine(rawInStream); - if (s.equals("none")) { - // no more header to parse; we're good to go - okay = true; - } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { - preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream); - if (preCompressStream != null) { - okay = true; - } - } else Slog.w(TAG, "Archive is encrypted but no password given"); - } else Slog.w(TAG, "Wrong header version: " + s); - } else Slog.w(TAG, "Didn't read the right header magic"); - - if (!okay) { - Slog.w(TAG, "Invalid restore data; aborting."); - return; - } - - // okay, use the right stream layer based on compression - in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream; - - boolean didRestore; - do { - didRestore = restoreOneFile(in, buffer); - } while (didRestore); - - if (MORE_DEBUG) Slog.v(TAG, "Done consuming input tarfile, total bytes=" + mBytes); - } catch (IOException e) { - Slog.e(TAG, "Unable to read restore input"); - } finally { - tearDownPipes(); - tearDownAgent(mTargetApp); - - try { - if (rawDataIn != null) rawDataIn.close(); - if (rawInStream != null) rawInStream.close(); - mInputFile.close(); - } catch (IOException e) { - Slog.w(TAG, "Close of restore data pipe threw", e); - /* nothing we can do about this */ - } - synchronized (mCurrentOpLock) { - mCurrentOperations.clear(); - } - synchronized (mLatchObject) { - mLatchObject.set(true); - mLatchObject.notifyAll(); - } - mObbConnection.tearDown(); - sendEndRestore(); - Slog.d(TAG, "Full restore pass complete."); - mWakelock.release(); - } - } - - String readHeaderLine(InputStream in) throws IOException { - int c; - StringBuilder buffer = new StringBuilder(80); - while ((c = in.read()) >= 0) { - if (c == '\n') break; // consume and discard the newlines - buffer.append((char)c); - } - return buffer.toString(); - } - - InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) { - InputStream result = null; - try { - if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { - - String userSaltHex = readHeaderLine(rawInStream); // 5 - byte[] userSalt = hexToByteArray(userSaltHex); - - String ckSaltHex = readHeaderLine(rawInStream); // 6 - byte[] ckSalt = hexToByteArray(ckSaltHex); - - int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7 - String userIvHex = readHeaderLine(rawInStream); // 8 - - String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 - - // decrypt the master key blob - Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); - SecretKey userKey = buildPasswordKey(mDecryptPassword, userSalt, - rounds); - byte[] IV = hexToByteArray(userIvHex); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - c.init(Cipher.DECRYPT_MODE, - new SecretKeySpec(userKey.getEncoded(), "AES"), - ivSpec); - byte[] mkCipher = hexToByteArray(masterKeyBlobHex); - byte[] mkBlob = c.doFinal(mkCipher); - - // first, the master key IV - int offset = 0; - int len = mkBlob[offset++]; - IV = Arrays.copyOfRange(mkBlob, offset, offset + len); - offset += len; - // then the master key itself - len = mkBlob[offset++]; - byte[] mk = Arrays.copyOfRange(mkBlob, - offset, offset + len); - offset += len; - // and finally the master key checksum hash - len = mkBlob[offset++]; - byte[] mkChecksum = Arrays.copyOfRange(mkBlob, - offset, offset + len); - - // now validate the decrypted master key against the checksum - byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds); - if (Arrays.equals(calculatedCk, mkChecksum)) { - ivSpec = new IvParameterSpec(IV); - c.init(Cipher.DECRYPT_MODE, - new SecretKeySpec(mk, "AES"), - ivSpec); - // Only if all of the above worked properly will 'result' be assigned - result = new CipherInputStream(rawInStream, c); - } else Slog.w(TAG, "Incorrect password"); - } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName); - } catch (InvalidAlgorithmParameterException e) { - Slog.e(TAG, "Needed parameter spec unavailable!", e); - } catch (BadPaddingException e) { - // This case frequently occurs when the wrong password is used to decrypt - // the master key. Use the identical "incorrect password" log text as is - // used in the checksum failure log in order to avoid providing additional - // information to an attacker. - Slog.w(TAG, "Incorrect password"); - } catch (IllegalBlockSizeException e) { - Slog.w(TAG, "Invalid block size in master key"); - } catch (NoSuchAlgorithmException e) { - Slog.e(TAG, "Needed decryption algorithm unavailable!"); - } catch (NoSuchPaddingException e) { - Slog.e(TAG, "Needed padding mechanism unavailable!"); - } catch (InvalidKeyException e) { - Slog.w(TAG, "Illegal password; aborting"); - } catch (NumberFormatException e) { - Slog.w(TAG, "Can't parse restore data header"); - } catch (IOException e) { - Slog.w(TAG, "Can't read input header"); - } - - return result; - } - - boolean restoreOneFile(InputStream instream, byte[] buffer) { - FileMetadata info; - try { - info = readTarHeaders(instream); - if (info != null) { - if (MORE_DEBUG) { - dumpFileMetadata(info); - } - - final String pkg = info.packageName; - if (!pkg.equals(mAgentPackage)) { - // okay, change in package; set up our various - // bookkeeping if we haven't seen it yet - if (!mPackagePolicies.containsKey(pkg)) { - mPackagePolicies.put(pkg, RestorePolicy.IGNORE); - } - - // Clean up the previous agent relationship if necessary, - // and let the observer know we're considering a new app. - if (mAgent != null) { - if (DEBUG) Slog.d(TAG, "Saw new package; tearing down old one"); - tearDownPipes(); - tearDownAgent(mTargetApp); - mTargetApp = null; - mAgentPackage = null; - } - } - - if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { - mPackagePolicies.put(pkg, readAppManifest(info, instream)); - mPackageInstallers.put(pkg, info.installerPackageName); - // We've read only the manifest content itself at this point, - // so consume the footer before looping around to the next - // input file - skipTarPadding(info.size, instream); - sendOnRestorePackage(pkg); - } else { - // Non-manifest, so it's actual file data. Is this a package - // we're ignoring? - boolean okay = true; - RestorePolicy policy = mPackagePolicies.get(pkg); - switch (policy) { - case IGNORE: - okay = false; - break; - - case ACCEPT_IF_APK: - // If we're in accept-if-apk state, then the first file we - // see MUST be the apk. - if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (DEBUG) Slog.d(TAG, "APK file; installing"); - // Try to install the app. - String installerName = mPackageInstallers.get(pkg); - okay = installApk(info, installerName, instream); - // good to go; promote to ACCEPT - mPackagePolicies.put(pkg, (okay) - ? RestorePolicy.ACCEPT - : RestorePolicy.IGNORE); - // At this point we've consumed this file entry - // ourselves, so just strip the tar footer and - // go on to the next file in the input stream - skipTarPadding(info.size, instream); - return true; - } else { - // File data before (or without) the apk. We can't - // handle it coherently in this case so ignore it. - mPackagePolicies.put(pkg, RestorePolicy.IGNORE); - okay = false; - } - break; - - case ACCEPT: - if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (DEBUG) Slog.d(TAG, "apk present but ACCEPT"); - // we can take the data without the apk, so we - // *want* to do so. skip the apk by declaring this - // one file not-okay without changing the restore - // policy for the package. - okay = false; - } - break; - - default: - // Something has gone dreadfully wrong when determining - // the restore policy from the manifest. Ignore the - // rest of this package's data. - Slog.e(TAG, "Invalid policy from manifest"); - okay = false; - mPackagePolicies.put(pkg, RestorePolicy.IGNORE); - break; - } - - // If the policy is satisfied, go ahead and set up to pipe the - // data to the agent. - if (DEBUG && okay && mAgent != null) { - Slog.i(TAG, "Reusing existing agent instance"); - } - if (okay && mAgent == null) { - if (DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg); - - try { - mTargetApp = mPackageManager.getApplicationInfo(pkg, 0); - - // If we haven't sent any data to this app yet, we probably - // need to clear it first. Check that. - if (!mClearedPackages.contains(pkg)) { - // apps with their own backup agents are - // responsible for coherently managing a full - // restore. - if (mTargetApp.backupAgentName == null) { - if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore"); - clearApplicationDataSynchronous(pkg); - } else { - if (DEBUG) Slog.d(TAG, "backup agent (" - + mTargetApp.backupAgentName + ") => no clear"); - } - mClearedPackages.add(pkg); - } else { - if (DEBUG) Slog.d(TAG, "We've initialized this app already; no clear required"); - } - - // All set; now set up the IPC and launch the agent - setUpPipes(); - mAgent = bindToAgentSynchronous(mTargetApp, - IApplicationThread.BACKUP_MODE_RESTORE_FULL); - mAgentPackage = pkg; - } catch (IOException e) { - // fall through to error handling - } catch (NameNotFoundException e) { - // fall through to error handling - } - - if (mAgent == null) { - if (DEBUG) Slog.d(TAG, "Unable to create agent for " + pkg); - okay = false; - tearDownPipes(); - mPackagePolicies.put(pkg, RestorePolicy.IGNORE); - } - } - - // Sanity check: make sure we never give data to the wrong app. This - // should never happen but a little paranoia here won't go amiss. - if (okay && !pkg.equals(mAgentPackage)) { - Slog.e(TAG, "Restoring data for " + pkg - + " but agent is for " + mAgentPackage); - okay = false; - } - - // At this point we have an agent ready to handle the full - // restore data as well as a pipe for sending data to - // that agent. Tell the agent to start reading from the - // pipe. - if (okay) { - boolean agentSuccess = true; - long toCopy = info.size; - final int token = generateToken(); - try { - prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); - if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) { - if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg - + " : " + info.path); - mObbConnection.restoreObbFile(pkg, mPipes[0], - info.size, info.type, info.path, info.mode, - info.mtime, token, mBackupManagerBinder); - } else { - if (DEBUG) Slog.d(TAG, "Invoking agent to restore file " - + info.path); - // fire up the app's agent listening on the socket. If - // the agent is running in the system process we can't - // just invoke it asynchronously, so we provide a thread - // for it here. - if (mTargetApp.processName.equals("system")) { - Slog.d(TAG, "system process agent - spinning a thread"); - RestoreFileRunnable runner = new RestoreFileRunnable( - mAgent, info, mPipes[0], token); - new Thread(runner).start(); - } else { - mAgent.doRestoreFile(mPipes[0], info.size, info.type, - info.domain, info.path, info.mode, info.mtime, - token, mBackupManagerBinder); - } - } - } catch (IOException e) { - // couldn't dup the socket for a process-local restore - Slog.d(TAG, "Couldn't establish restore"); - agentSuccess = false; - okay = false; - } catch (RemoteException e) { - // whoops, remote entity went away. We'll eat the content - // ourselves, then, and not copy it over. - Slog.e(TAG, "Agent crashed during full restore"); - agentSuccess = false; - okay = false; - } - - // Copy over the data if the agent is still good - if (okay) { - boolean pipeOkay = true; - FileOutputStream pipe = new FileOutputStream( - mPipes[1].getFileDescriptor()); - while (toCopy > 0) { - int toRead = (toCopy > buffer.length) - ? buffer.length : (int)toCopy; - int nRead = instream.read(buffer, 0, toRead); - if (nRead >= 0) mBytes += nRead; - if (nRead <= 0) break; - toCopy -= nRead; - - // send it to the output pipe as long as things - // are still good - if (pipeOkay) { - try { - pipe.write(buffer, 0, nRead); - } catch (IOException e) { - Slog.e(TAG, "Failed to write to restore pipe", e); - pipeOkay = false; - } - } - } - - // done sending that file! Now we just need to consume - // the delta from info.size to the end of block. - skipTarPadding(info.size, instream); - - // and now that we've sent it all, wait for the remote - // side to acknowledge receipt - agentSuccess = waitUntilOperationComplete(token); - } - - // okay, if the remote end failed at any point, deal with - // it by ignoring the rest of the restore on it - if (!agentSuccess) { - mBackupHandler.removeMessages(MSG_TIMEOUT); - tearDownPipes(); - tearDownAgent(mTargetApp); - mAgent = null; - mPackagePolicies.put(pkg, RestorePolicy.IGNORE); - } - } - - // Problems setting up the agent communication, or an already- - // ignored package: skip to the next tar stream entry by - // reading and discarding this file. - if (!okay) { - if (DEBUG) Slog.d(TAG, "[discarding file content]"); - long bytesToConsume = (info.size + 511) & ~511; - while (bytesToConsume > 0) { - int toRead = (bytesToConsume > buffer.length) - ? buffer.length : (int)bytesToConsume; - long nRead = instream.read(buffer, 0, toRead); - if (nRead >= 0) mBytes += nRead; - if (nRead <= 0) break; - bytesToConsume -= nRead; - } - } - } - } - } catch (IOException e) { - if (DEBUG) Slog.w(TAG, "io exception on restore socket read", e); - // treat as EOF - info = null; - } - - return (info != null); - } - - void setUpPipes() throws IOException { - mPipes = ParcelFileDescriptor.createPipe(); - } - - void tearDownPipes() { - if (mPipes != null) { - try { - mPipes[0].close(); - mPipes[0] = null; - mPipes[1].close(); - mPipes[1] = null; - } catch (IOException e) { - Slog.w(TAG, "Couldn't close agent pipes", e); - } - mPipes = null; - } - } - - void tearDownAgent(ApplicationInfo app) { - if (mAgent != null) { - try { - // unbind and tidy up even on timeout or failure, just in case - mActivityManager.unbindBackupAgent(app); - - // The agent was running with a stub Application object, so shut it down. - // !!! We hardcode the confirmation UI's package name here rather than use a - // manifest flag! TODO something less direct. - if (app.uid != Process.SYSTEM_UID - && !app.packageName.equals("com.android.backupconfirm")) { - if (DEBUG) Slog.d(TAG, "Killing host process"); - mActivityManager.killApplicationProcess(app.processName, app.uid); - } else { - if (DEBUG) Slog.d(TAG, "Not killing after full restore"); - } - } catch (RemoteException e) { - Slog.d(TAG, "Lost app trying to shut down"); - } - mAgent = null; - } - } - - class RestoreInstallObserver extends IPackageInstallObserver.Stub { - final AtomicBoolean mDone = new AtomicBoolean(); - String mPackageName; - int mResult; - - public void reset() { - synchronized (mDone) { - mDone.set(false); - } - } - - public void waitForCompletion() { - synchronized (mDone) { - while (mDone.get() == false) { - try { - mDone.wait(); - } catch (InterruptedException e) { } - } - } - } - - int getResult() { - return mResult; - } - - @Override - public void packageInstalled(String packageName, int returnCode) - throws RemoteException { - synchronized (mDone) { - mResult = returnCode; - mPackageName = packageName; - mDone.set(true); - mDone.notifyAll(); - } - } - } - - class RestoreDeleteObserver extends IPackageDeleteObserver.Stub { - final AtomicBoolean mDone = new AtomicBoolean(); - int mResult; - - public void reset() { - synchronized (mDone) { - mDone.set(false); - } - } - - public void waitForCompletion() { - synchronized (mDone) { - while (mDone.get() == false) { - try { - mDone.wait(); - } catch (InterruptedException e) { } - } - } - } - - @Override - public void packageDeleted(String packageName, int returnCode) throws RemoteException { - synchronized (mDone) { - mResult = returnCode; - mDone.set(true); - mDone.notifyAll(); - } - } - } - - final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); - final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); - - boolean installApk(FileMetadata info, String installerPackage, InputStream instream) { - boolean okay = true; - - if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName); - - // The file content is an .apk file. Copy it out to a staging location and - // attempt to install it. - File apkFile = new File(mDataDir, info.packageName); - try { - FileOutputStream apkStream = new FileOutputStream(apkFile); - byte[] buffer = new byte[32 * 1024]; - long size = info.size; - while (size > 0) { - long toRead = (buffer.length < size) ? buffer.length : size; - int didRead = instream.read(buffer, 0, (int)toRead); - if (didRead >= 0) mBytes += didRead; - apkStream.write(buffer, 0, didRead); - size -= didRead; - } - apkStream.close(); - - // make sure the installer can read it - apkFile.setReadable(true, false); - - // Now install it - Uri packageUri = Uri.fromFile(apkFile); - mInstallObserver.reset(); - mPackageManager.installPackage(packageUri, mInstallObserver, - PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB, - installerPackage); - mInstallObserver.waitForCompletion(); - - if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) { - // The only time we continue to accept install of data even if the - // apk install failed is if we had already determined that we could - // accept the data regardless. - if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) { - okay = false; - } - } else { - // Okay, the install succeeded. Make sure it was the right app. - boolean uninstall = false; - if (!mInstallObserver.mPackageName.equals(info.packageName)) { - Slog.w(TAG, "Restore stream claimed to include apk for " - + info.packageName + " but apk was really " - + mInstallObserver.mPackageName); - // delete the package we just put in place; it might be fraudulent - okay = false; - uninstall = true; - } else { - try { - PackageInfo pkg = mPackageManager.getPackageInfo(info.packageName, - PackageManager.GET_SIGNATURES); - if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) { - Slog.w(TAG, "Restore stream contains apk of package " - + info.packageName + " but it disallows backup/restore"); - okay = false; - } else { - // So far so good -- do the signatures match the manifest? - Signature[] sigs = mManifestSignatures.get(info.packageName); - if (signaturesMatch(sigs, pkg)) { - // If this is a system-uid app without a declared backup agent, - // don't restore any of the file data. - if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) - && (pkg.applicationInfo.backupAgentName == null)) { - Slog.w(TAG, "Installed app " + info.packageName - + " has restricted uid and no agent"); - okay = false; - } - } else { - Slog.w(TAG, "Installed app " + info.packageName - + " signatures do not match restore manifest"); - okay = false; - uninstall = true; - } - } - } catch (NameNotFoundException e) { - Slog.w(TAG, "Install of package " + info.packageName - + " succeeded but now not found"); - okay = false; - } - } - - // If we're not okay at this point, we need to delete the package - // that we just installed. - if (uninstall) { - mDeleteObserver.reset(); - mPackageManager.deletePackage(mInstallObserver.mPackageName, - mDeleteObserver, 0); - mDeleteObserver.waitForCompletion(); - } - } - } catch (IOException e) { - Slog.e(TAG, "Unable to transcribe restored apk for install"); - okay = false; - } finally { - apkFile.delete(); - } - - return okay; - } - - // Given an actual file content size, consume the post-content padding mandated - // by the tar format. - void skipTarPadding(long size, InputStream instream) throws IOException { - long partial = (size + 512) % 512; - if (partial > 0) { - final int needed = 512 - (int)partial; - byte[] buffer = new byte[needed]; - if (readExactly(instream, buffer, 0, needed) == needed) { - mBytes += needed; - } else throw new IOException("Unexpected EOF in padding"); - } - } - - // Returns a policy constant; takes a buffer arg to reduce memory churn - RestorePolicy readAppManifest(FileMetadata info, InputStream instream) - throws IOException { - // Fail on suspiciously large manifest files - if (info.size > 64 * 1024) { - throw new IOException("Restore manifest too big; corrupt? size=" + info.size); - } - - byte[] buffer = new byte[(int) info.size]; - if (readExactly(instream, buffer, 0, (int)info.size) == info.size) { - mBytes += info.size; - } else throw new IOException("Unexpected EOF in manifest"); - - RestorePolicy policy = RestorePolicy.IGNORE; - String[] str = new String[1]; - int offset = 0; - - try { - offset = extractLine(buffer, offset, str); - int version = Integer.parseInt(str[0]); - if (version == BACKUP_MANIFEST_VERSION) { - offset = extractLine(buffer, offset, str); - String manifestPackage = str[0]; - // TODO: handle - if (manifestPackage.equals(info.packageName)) { - offset = extractLine(buffer, offset, str); - version = Integer.parseInt(str[0]); // app version - offset = extractLine(buffer, offset, str); - int platformVersion = Integer.parseInt(str[0]); - offset = extractLine(buffer, offset, str); - info.installerPackageName = (str[0].length() > 0) ? str[0] : null; - offset = extractLine(buffer, offset, str); - boolean hasApk = str[0].equals("1"); - offset = extractLine(buffer, offset, str); - int numSigs = Integer.parseInt(str[0]); - if (numSigs > 0) { - Signature[] sigs = new Signature[numSigs]; - for (int i = 0; i < numSigs; i++) { - offset = extractLine(buffer, offset, str); - sigs[i] = new Signature(str[0]); - } - mManifestSignatures.put(info.packageName, sigs); - - // Okay, got the manifest info we need... - try { - PackageInfo pkgInfo = mPackageManager.getPackageInfo( - info.packageName, PackageManager.GET_SIGNATURES); - // Fall through to IGNORE if the app explicitly disallows backup - final int flags = pkgInfo.applicationInfo.flags; - if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) { - // Restore system-uid-space packages only if they have - // defined a custom backup agent - if ((pkgInfo.applicationInfo.uid >= Process.FIRST_APPLICATION_UID) - || (pkgInfo.applicationInfo.backupAgentName != null)) { - // Verify signatures against any installed version; if they - // don't match, then we fall though and ignore the data. The - // signatureMatch() method explicitly ignores the signature - // check for packages installed on the system partition, because - // such packages are signed with the platform cert instead of - // the app developer's cert, so they're different on every - // device. - if (signaturesMatch(sigs, pkgInfo)) { - if (pkgInfo.versionCode >= version) { - Slog.i(TAG, "Sig + version match; taking data"); - policy = RestorePolicy.ACCEPT; - } else { - // The data is from a newer version of the app than - // is presently installed. That means we can only - // use it if the matching apk is also supplied. - Slog.d(TAG, "Data version " + version - + " is newer than installed version " - + pkgInfo.versionCode + " - requiring apk"); - policy = RestorePolicy.ACCEPT_IF_APK; - } - } else { - Slog.w(TAG, "Restore manifest signatures do not match " - + "installed application for " + info.packageName); - } - } else { - Slog.w(TAG, "Package " + info.packageName - + " is system level with no agent"); - } - } else { - if (DEBUG) Slog.i(TAG, "Restore manifest from " - + info.packageName + " but allowBackup=false"); - } - } catch (NameNotFoundException e) { - // Okay, the target app isn't installed. We can process - // the restore properly only if the dataset provides the - // apk file and we can successfully install it. - if (DEBUG) Slog.i(TAG, "Package " + info.packageName - + " not installed; requiring apk in dataset"); - policy = RestorePolicy.ACCEPT_IF_APK; - } - - if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) { - Slog.i(TAG, "Cannot restore package " + info.packageName - + " without the matching .apk"); - } - } else { - Slog.i(TAG, "Missing signature on backed-up package " - + info.packageName); - } - } else { - Slog.i(TAG, "Expected package " + info.packageName - + " but restore manifest claims " + manifestPackage); - } - } else { - Slog.i(TAG, "Unknown restore manifest version " + version - + " for package " + info.packageName); - } - } catch (NumberFormatException e) { - Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); - } catch (IllegalArgumentException e) { - Slog.w(TAG, e.getMessage()); - } - - return policy; - } - - // Builds a line from a byte buffer starting at 'offset', and returns - // the index of the next unconsumed data in the buffer. - int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException { - final int end = buffer.length; - if (offset >= end) throw new IOException("Incomplete data"); - - int pos; - for (pos = offset; pos < end; pos++) { - byte c = buffer[pos]; - // at LF we declare end of line, and return the next char as the - // starting point for the next time through - if (c == '\n') { - break; - } - } - outStr[0] = new String(buffer, offset, pos - offset); - pos++; // may be pointing an extra byte past the end but that's okay - return pos; - } - - void dumpFileMetadata(FileMetadata info) { - if (DEBUG) { - StringBuilder b = new StringBuilder(128); - - // mode string - b.append((info.type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-'); - b.append(((info.mode & 0400) != 0) ? 'r' : '-'); - b.append(((info.mode & 0200) != 0) ? 'w' : '-'); - b.append(((info.mode & 0100) != 0) ? 'x' : '-'); - b.append(((info.mode & 0040) != 0) ? 'r' : '-'); - b.append(((info.mode & 0020) != 0) ? 'w' : '-'); - b.append(((info.mode & 0010) != 0) ? 'x' : '-'); - b.append(((info.mode & 0004) != 0) ? 'r' : '-'); - b.append(((info.mode & 0002) != 0) ? 'w' : '-'); - b.append(((info.mode & 0001) != 0) ? 'x' : '-'); - b.append(String.format(" %9d ", info.size)); - - Date stamp = new Date(info.mtime); - b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp)); - - b.append(info.packageName); - b.append(" :: "); - b.append(info.domain); - b.append(" :: "); - b.append(info.path); - - Slog.i(TAG, b.toString()); - } - } - // Consume a tar file header block [sequence] and accumulate the relevant metadata - FileMetadata readTarHeaders(InputStream instream) throws IOException { - byte[] block = new byte[512]; - FileMetadata info = null; - - boolean gotHeader = readTarHeader(instream, block); - if (gotHeader) { - try { - // okay, presume we're okay, and extract the various metadata - info = new FileMetadata(); - info.size = extractRadix(block, 124, 12, 8); - info.mtime = extractRadix(block, 136, 12, 8); - info.mode = extractRadix(block, 100, 8, 8); - - info.path = extractString(block, 345, 155); // prefix - String path = extractString(block, 0, 100); - if (path.length() > 0) { - if (info.path.length() > 0) info.path += '/'; - info.path += path; - } - - // tar link indicator field: 1 byte at offset 156 in the header. - int typeChar = block[156]; - if (typeChar == 'x') { - // pax extended header, so we need to read that - gotHeader = readPaxExtendedHeader(instream, info); - if (gotHeader) { - // and after a pax extended header comes another real header -- read - // that to find the real file type - gotHeader = readTarHeader(instream, block); - } - if (!gotHeader) throw new IOException("Bad or missing pax header"); - - typeChar = block[156]; - } - - switch (typeChar) { - case '0': info.type = BackupAgent.TYPE_FILE; break; - case '5': { - info.type = BackupAgent.TYPE_DIRECTORY; - if (info.size != 0) { - Slog.w(TAG, "Directory entry with nonzero size in header"); - info.size = 0; - } - break; - } - case 0: { - // presume EOF - if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info); - return null; - } - default: { - Slog.e(TAG, "Unknown tar entity type: " + typeChar); - throw new IOException("Unknown entity type " + typeChar); - } - } - - // Parse out the path - // - // first: apps/shared/unrecognized - if (FullBackup.SHARED_PREFIX.regionMatches(0, - info.path, 0, FullBackup.SHARED_PREFIX.length())) { - // File in shared storage. !!! TODO: implement this. - info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); - info.packageName = SHARED_BACKUP_AGENT_PACKAGE; - info.domain = FullBackup.SHARED_STORAGE_TOKEN; - if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path); - } else if (FullBackup.APPS_PREFIX.regionMatches(0, - info.path, 0, FullBackup.APPS_PREFIX.length())) { - // App content! Parse out the package name and domain - - // strip the apps/ prefix - info.path = info.path.substring(FullBackup.APPS_PREFIX.length()); - - // extract the package name - int slash = info.path.indexOf('/'); - if (slash < 0) throw new IOException("Illegal semantic path in " + info.path); - info.packageName = info.path.substring(0, slash); - info.path = info.path.substring(slash+1); - - // if it's a manifest we're done, otherwise parse out the domains - if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) { - slash = info.path.indexOf('/'); - if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path); - info.domain = info.path.substring(0, slash); - info.path = info.path.substring(slash + 1); - } - } - } catch (IOException e) { - if (DEBUG) { - Slog.e(TAG, "Parse error in header: " + e.getMessage()); - HEXLOG(block); - } - throw e; - } - } - return info; - } - - private void HEXLOG(byte[] block) { - int offset = 0; - int todo = block.length; - StringBuilder buf = new StringBuilder(64); - while (todo > 0) { - buf.append(String.format("%04x ", offset)); - int numThisLine = (todo > 16) ? 16 : todo; - for (int i = 0; i < numThisLine; i++) { - buf.append(String.format("%02x ", block[offset+i])); - } - Slog.i("hexdump", buf.toString()); - buf.setLength(0); - todo -= numThisLine; - offset += numThisLine; - } - } - - // Read exactly the given number of bytes into a buffer at the stated offset. - // Returns false if EOF is encountered before the requested number of bytes - // could be read. - int readExactly(InputStream in, byte[] buffer, int offset, int size) - throws IOException { - if (size <= 0) throw new IllegalArgumentException("size must be > 0"); - - int soFar = 0; - while (soFar < size) { - int nRead = in.read(buffer, offset + soFar, size - soFar); - if (nRead <= 0) { - if (MORE_DEBUG) Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar); - break; - } - soFar += nRead; - } - return soFar; - } - - boolean readTarHeader(InputStream instream, byte[] block) throws IOException { - final int got = readExactly(instream, block, 0, 512); - if (got == 0) return false; // Clean EOF - if (got < 512) throw new IOException("Unable to read full block header"); - mBytes += 512; - return true; - } - - // overwrites 'info' fields based on the pax extended header - boolean readPaxExtendedHeader(InputStream instream, FileMetadata info) - throws IOException { - // We should never see a pax extended header larger than this - if (info.size > 32*1024) { - Slog.w(TAG, "Suspiciously large pax header size " + info.size - + " - aborting"); - throw new IOException("Sanity failure: pax header size " + info.size); - } - - // read whole blocks, not just the content size - int numBlocks = (int)((info.size + 511) >> 9); - byte[] data = new byte[numBlocks * 512]; - if (readExactly(instream, data, 0, data.length) < data.length) { - throw new IOException("Unable to read full pax header"); - } - mBytes += data.length; - - final int contentSize = (int) info.size; - int offset = 0; - do { - // extract the line at 'offset' - int eol = offset+1; - while (eol < contentSize && data[eol] != ' ') eol++; - if (eol >= contentSize) { - // error: we just hit EOD looking for the end of the size field - throw new IOException("Invalid pax data"); - } - // eol points to the space between the count and the key - int linelen = (int) extractRadix(data, offset, eol - offset, 10); - int key = eol + 1; // start of key=value - eol = offset + linelen - 1; // trailing LF - int value; - for (value = key+1; data[value] != '=' && value <= eol; value++); - if (value > eol) { - throw new IOException("Invalid pax declaration"); - } - - // pax requires that key/value strings be in UTF-8 - String keyStr = new String(data, key, value-key, "UTF-8"); - // -1 to strip the trailing LF - String valStr = new String(data, value+1, eol-value-1, "UTF-8"); - - if ("path".equals(keyStr)) { - info.path = valStr; - } else if ("size".equals(keyStr)) { - info.size = Long.parseLong(valStr); - } else { - if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key); - } - - offset += linelen; - } while (offset < contentSize); - - return true; - } - - long extractRadix(byte[] data, int offset, int maxChars, int radix) - throws IOException { - long value = 0; - final int end = offset + maxChars; - for (int i = offset; i < end; i++) { - final byte b = data[i]; - // Numeric fields in tar can terminate with either NUL or SPC - if (b == 0 || b == ' ') break; - if (b < '0' || b > ('0' + radix - 1)) { - throw new IOException("Invalid number in header: '" + (char)b + "' for radix " + radix); - } - value = radix * value + (b - '0'); - } - return value; - } - - String extractString(byte[] data, int offset, int maxChars) throws IOException { - final int end = offset + maxChars; - int eos = offset; - // tar string fields terminate early with a NUL - while (eos < end && data[eos] != 0) eos++; - return new String(data, offset, eos-offset, "US-ASCII"); - } - - void sendStartRestore() { - if (mObserver != null) { - try { - mObserver.onStartRestore(); - } catch (RemoteException e) { - Slog.w(TAG, "full restore observer went away: startRestore"); - mObserver = null; - } - } - } - - void sendOnRestorePackage(String name) { - if (mObserver != null) { - try { - // TODO: use a more user-friendly name string - mObserver.onRestorePackage(name); - } catch (RemoteException e) { - Slog.w(TAG, "full restore observer went away: restorePackage"); - mObserver = null; - } - } - } - - void sendEndRestore() { - if (mObserver != null) { - try { - mObserver.onEndRestore(); - } catch (RemoteException e) { - Slog.w(TAG, "full restore observer went away: endRestore"); - mObserver = null; - } - } - } - } - - // ----- Restore handling ----- - - private boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) { - // If the target resides on the system partition, we allow it to restore - // data from the like-named package in a restore set even if the signatures - // do not match. (Unlike general applications, those flashed to the system - // partition will be signed with the device's platform certificate, so on - // different phones the same system app will have different signatures.) - if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - if (DEBUG) Slog.v(TAG, "System app " + target.packageName + " - skipping sig check"); - return true; - } - - // Allow unsigned apps, but not signed on one device and unsigned on the other - // !!! TODO: is this the right policy? - Signature[] deviceSigs = target.signatures; - if (MORE_DEBUG) Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs - + " device=" + deviceSigs); - if ((storedSigs == null || storedSigs.length == 0) - && (deviceSigs == null || deviceSigs.length == 0)) { - return true; - } - if (storedSigs == null || deviceSigs == null) { - return false; - } - - // !!! TODO: this demands that every stored signature match one - // that is present on device, and does not demand the converse. - // Is this this right policy? - int nStored = storedSigs.length; - int nDevice = deviceSigs.length; - - for (int i=0; i < nStored; i++) { - boolean match = false; - for (int j=0; j < nDevice; j++) { - if (storedSigs[i].equals(deviceSigs[j])) { - match = true; - break; - } - } - if (!match) { - return false; - } - } - return true; - } - - enum RestoreState { - INITIAL, - DOWNLOAD_DATA, - PM_METADATA, - RUNNING_QUEUE, - FINAL - } - - class PerformRestoreTask implements BackupRestoreTask { - private IBackupTransport mTransport; - private IRestoreObserver mObserver; - private long mToken; - private PackageInfo mTargetPackage; - private File mStateDir; - private int mPmToken; - private boolean mNeedFullBackup; - private HashSet mFilterSet; - private long mStartRealtime; - private PackageManagerBackupAgent mPmAgent; - private List mAgentPackages; - private ArrayList mRestorePackages; - private RestoreState mCurrentState; - private int mCount; - private boolean mFinished; - private int mStatus; - private File mBackupDataName; - private File mNewStateName; - private File mSavedStateName; - private ParcelFileDescriptor mBackupData; - private ParcelFileDescriptor mNewState; - private PackageInfo mCurrentPackage; - - - class RestoreRequest { - public PackageInfo app; - public int storedAppVersion; - - RestoreRequest(PackageInfo _app, int _version) { - app = _app; - storedAppVersion = _version; - } - } - - PerformRestoreTask(IBackupTransport transport, String dirName, IRestoreObserver observer, - long restoreSetToken, PackageInfo targetPackage, int pmToken, - boolean needFullBackup, String[] filterSet) { - mCurrentState = RestoreState.INITIAL; - mFinished = false; - mPmAgent = null; - - mTransport = transport; - mObserver = observer; - mToken = restoreSetToken; - mTargetPackage = targetPackage; - mPmToken = pmToken; - mNeedFullBackup = needFullBackup; - - if (filterSet != null) { - mFilterSet = new HashSet(); - for (String pkg : filterSet) { - mFilterSet.add(pkg); - } - } else { - mFilterSet = null; - } - - mStateDir = new File(mBaseStateDir, dirName); - } - - // Execute one tick of whatever state machine the task implements - @Override - public void execute() { - if (MORE_DEBUG) Slog.v(TAG, "*** Executing restore step: " + mCurrentState); - switch (mCurrentState) { - case INITIAL: - beginRestore(); - break; - - case DOWNLOAD_DATA: - downloadRestoreData(); - break; - - case PM_METADATA: - restorePmMetadata(); - break; - - case RUNNING_QUEUE: - restoreNextAgent(); - break; - - case FINAL: - if (!mFinished) finalizeRestore(); - else { - Slog.e(TAG, "Duplicate finish"); - } - mFinished = true; - break; - } - } - - // Initialize and set up for the PM metadata restore, which comes first - void beginRestore() { - // Don't account time doing the restore as inactivity of the app - // that has opened a restore session. - mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); - - // Assume error until we successfully init everything - mStatus = BackupConstants.TRANSPORT_ERROR; - - try { - // TODO: Log this before getAvailableRestoreSets, somehow - EventLog.writeEvent(EventLogTags.RESTORE_START, mTransport.transportDirName(), mToken); - - // Get the list of all packages which have backup enabled. - // (Include the Package Manager metadata pseudo-package first.) - mRestorePackages = new ArrayList(); - PackageInfo omPackage = new PackageInfo(); - omPackage.packageName = PACKAGE_MANAGER_SENTINEL; - mRestorePackages.add(omPackage); - - mAgentPackages = allAgentPackages(); - if (mTargetPackage == null) { - // if there's a filter set, strip out anything that isn't - // present before proceeding - if (mFilterSet != null) { - for (int i = mAgentPackages.size() - 1; i >= 0; i--) { - final PackageInfo pkg = mAgentPackages.get(i); - if (! mFilterSet.contains(pkg.packageName)) { - mAgentPackages.remove(i); - } - } - if (MORE_DEBUG) { - Slog.i(TAG, "Post-filter package set for restore:"); - for (PackageInfo p : mAgentPackages) { - Slog.i(TAG, " " + p); - } - } - } - mRestorePackages.addAll(mAgentPackages); - } else { - // Just one package to attempt restore of - mRestorePackages.add(mTargetPackage); - } - - // let the observer know that we're running - if (mObserver != null) { - try { - // !!! TODO: get an actual count from the transport after - // its startRestore() runs? - mObserver.restoreStarting(mRestorePackages.size()); - } catch (RemoteException e) { - Slog.d(TAG, "Restore observer died at restoreStarting"); - mObserver = null; - } - } - } catch (RemoteException e) { - // Something has gone catastrophically wrong with the transport - Slog.e(TAG, "Error communicating with transport for restore"); - executeNextState(RestoreState.FINAL); - return; - } - - mStatus = BackupConstants.TRANSPORT_OK; - executeNextState(RestoreState.DOWNLOAD_DATA); - } - - void downloadRestoreData() { - // Note that the download phase can be very time consuming, but we're executing - // it inline here on the looper. This is "okay" because it is not calling out to - // third party code; the transport is "trusted," and so we assume it is being a - // good citizen and timing out etc when appropriate. - // - // TODO: when appropriate, move the download off the looper and rearrange the - // error handling around that. - try { - mStatus = mTransport.startRestore(mToken, - mRestorePackages.toArray(new PackageInfo[0])); - if (mStatus != BackupConstants.TRANSPORT_OK) { - Slog.e(TAG, "Error starting restore operation"); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - executeNextState(RestoreState.FINAL); - return; - } - } catch (RemoteException e) { - Slog.e(TAG, "Error communicating with transport for restore"); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - mStatus = BackupConstants.TRANSPORT_ERROR; - executeNextState(RestoreState.FINAL); - return; - } - - // Successful download of the data to be parceled out to the apps, so off we go. - executeNextState(RestoreState.PM_METADATA); - } - - void restorePmMetadata() { - try { - String packageName = mTransport.nextRestorePackage(); - if (packageName == null) { - Slog.e(TAG, "Error getting first restore package"); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - mStatus = BackupConstants.TRANSPORT_ERROR; - executeNextState(RestoreState.FINAL); - return; - } else if (packageName.equals("")) { - Slog.i(TAG, "No restore data available"); - int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); - EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, 0, millis); - mStatus = BackupConstants.TRANSPORT_OK; - executeNextState(RestoreState.FINAL); - return; - } else if (!packageName.equals(PACKAGE_MANAGER_SENTINEL)) { - Slog.e(TAG, "Expected restore data for \"" + PACKAGE_MANAGER_SENTINEL - + "\", found only \"" + packageName + "\""); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, - "Package manager data missing"); - executeNextState(RestoreState.FINAL); - return; - } - - // Pull the Package Manager metadata from the restore set first - PackageInfo omPackage = new PackageInfo(); - omPackage.packageName = PACKAGE_MANAGER_SENTINEL; - mPmAgent = new PackageManagerBackupAgent( - mPackageManager, mAgentPackages); - initiateOneRestore(omPackage, 0, IBackupAgent.Stub.asInterface(mPmAgent.onBind()), - mNeedFullBackup); - // The PM agent called operationComplete() already, because our invocation - // of it is process-local and therefore synchronous. That means that a - // RUNNING_QUEUE message is already enqueued. Only if we're unable to - // proceed with running the queue do we remove that pending message and - // jump straight to the FINAL state. - - // Verify that the backup set includes metadata. If not, we can't do - // signature/version verification etc, so we simply do not proceed with - // the restore operation. - if (!mPmAgent.hasMetadata()) { - Slog.e(TAG, "No restore metadata available, so not restoring settings"); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, - "Package manager restore metadata missing"); - mStatus = BackupConstants.TRANSPORT_ERROR; - mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); - executeNextState(RestoreState.FINAL); - return; - } - } catch (RemoteException e) { - Slog.e(TAG, "Error communicating with transport for restore"); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - mStatus = BackupConstants.TRANSPORT_ERROR; - mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); - executeNextState(RestoreState.FINAL); - return; - } - - // Metadata is intact, so we can now run the restore queue. If we get here, - // we have already enqueued the necessary next-step message on the looper. - } - - void restoreNextAgent() { - try { - String packageName = mTransport.nextRestorePackage(); - - if (packageName == null) { - Slog.e(TAG, "Error getting next restore package"); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - executeNextState(RestoreState.FINAL); - return; - } else if (packageName.equals("")) { - if (DEBUG) Slog.v(TAG, "No next package, finishing restore"); - int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); - EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis); - executeNextState(RestoreState.FINAL); - return; - } - - if (mObserver != null) { - try { - mObserver.onUpdate(mCount, packageName); - } catch (RemoteException e) { - Slog.d(TAG, "Restore observer died in onUpdate"); - mObserver = null; - } - } - - Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName); - if (metaInfo == null) { - Slog.e(TAG, "Missing metadata for " + packageName); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, - "Package metadata missing"); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } - - PackageInfo packageInfo; - try { - int flags = PackageManager.GET_SIGNATURES; - packageInfo = mPackageManager.getPackageInfo(packageName, flags); - } catch (NameNotFoundException e) { - Slog.e(TAG, "Invalid package restoring data", e); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, - "Package missing on device"); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } - - if (packageInfo.applicationInfo.backupAgentName == null - || "".equals(packageInfo.applicationInfo.backupAgentName)) { - if (DEBUG) { - Slog.i(TAG, "Data exists for package " + packageName - + " but app has no agent; skipping"); - } - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, - "Package has no agent"); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } - - if (metaInfo.versionCode > packageInfo.versionCode) { - // Data is from a "newer" version of the app than we have currently - // installed. If the app has not declared that it is prepared to - // handle this case, we do not attempt the restore. - if ((packageInfo.applicationInfo.flags - & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) { - String message = "Version " + metaInfo.versionCode - + " > installed version " + packageInfo.versionCode; - Slog.w(TAG, "Package " + packageName + ": " + message); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, - packageName, message); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } else { - if (DEBUG) Slog.v(TAG, "Version " + metaInfo.versionCode - + " > installed " + packageInfo.versionCode - + " but restoreAnyVersion"); - } - } - - if (!signaturesMatch(metaInfo.signatures, packageInfo)) { - Slog.w(TAG, "Signature mismatch restoring " + packageName); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, - "Signature mismatch"); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } - - if (DEBUG) Slog.v(TAG, "Package " + packageName - + " restore version [" + metaInfo.versionCode - + "] is compatible with installed version [" - + packageInfo.versionCode + "]"); - - // Then set up and bind the agent - IBackupAgent agent = bindToAgentSynchronous( - packageInfo.applicationInfo, - IApplicationThread.BACKUP_MODE_INCREMENTAL); - if (agent == null) { - Slog.w(TAG, "Can't find backup agent for " + packageName); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, - "Restore agent missing"); - executeNextState(RestoreState.RUNNING_QUEUE); - return; - } - - // And then finally start the restore on this agent - try { - initiateOneRestore(packageInfo, metaInfo.versionCode, agent, mNeedFullBackup); - ++mCount; - } catch (Exception e) { - Slog.e(TAG, "Error when attempting restore: " + e.toString()); - agentErrorCleanup(); - executeNextState(RestoreState.RUNNING_QUEUE); - } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to fetch restore data from transport"); - mStatus = BackupConstants.TRANSPORT_ERROR; - executeNextState(RestoreState.FINAL); - } - } - - void finalizeRestore() { - if (MORE_DEBUG) Slog.d(TAG, "finishing restore mObserver=" + mObserver); - - try { - mTransport.finishRestore(); - } catch (RemoteException e) { - Slog.e(TAG, "Error finishing restore", e); - } - - if (mObserver != null) { - try { - mObserver.restoreFinished(mStatus); - } catch (RemoteException e) { - Slog.d(TAG, "Restore observer died at restoreFinished"); - } - } - - // If this was a restoreAll operation, record that this was our - // ancestral dataset, as well as the set of apps that are possibly - // restoreable from the dataset - if (mTargetPackage == null && mPmAgent != null) { - mAncestralPackages = mPmAgent.getRestoredPackages(); - mAncestralToken = mToken; - writeRestoreTokens(); - } - - // We must under all circumstances tell the Package Manager to - // proceed with install notifications if it's waiting for us. - if (mPmToken > 0) { - if (MORE_DEBUG) Slog.v(TAG, "finishing PM token " + mPmToken); - try { - mPackageManagerBinder.finishPackageInstall(mPmToken); - } catch (RemoteException e) { /* can't happen */ } - } - - // Furthermore we need to reset the session timeout clock - mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); - mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, - TIMEOUT_RESTORE_INTERVAL); - - // done; we can finally release the wakelock - Slog.i(TAG, "Restore complete."); - mWakelock.release(); - } - - // Call asynchronously into the app, passing it the restore data. The next step - // after this is always a callback, either operationComplete() or handleTimeout(). - void initiateOneRestore(PackageInfo app, int appVersionCode, IBackupAgent agent, - boolean needFullBackup) { - mCurrentPackage = app; - final String packageName = app.packageName; - - if (DEBUG) Slog.d(TAG, "initiateOneRestore packageName=" + packageName); - - // !!! TODO: get the dirs from the transport - mBackupDataName = new File(mDataDir, packageName + ".restore"); - mNewStateName = new File(mStateDir, packageName + ".new"); - mSavedStateName = new File(mStateDir, packageName); - - final int token = generateToken(); - try { - // Run the transport's restore pass - mBackupData = ParcelFileDescriptor.open(mBackupDataName, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - if (!SELinux.restorecon(mBackupDataName)) { - Slog.e(TAG, "SElinux restorecon failed for " + mBackupDataName); - } - - if (mTransport.getRestoreData(mBackupData) != BackupConstants.TRANSPORT_OK) { - // Transport-level failure, so we wind everything up and - // terminate the restore operation. - Slog.e(TAG, "Error getting restore data for " + packageName); - EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); - mBackupData.close(); - mBackupDataName.delete(); - executeNextState(RestoreState.FINAL); - return; - } - - // Okay, we have the data. Now have the agent do the restore. - mBackupData.close(); - mBackupData = ParcelFileDescriptor.open(mBackupDataName, - ParcelFileDescriptor.MODE_READ_ONLY); - - mNewState = ParcelFileDescriptor.open(mNewStateName, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - // Kick off the restore, checking for hung agents - prepareOperationTimeout(token, TIMEOUT_RESTORE_INTERVAL, this); - agent.doRestore(mBackupData, appVersionCode, mNewState, token, mBackupManagerBinder); - } catch (Exception e) { - Slog.e(TAG, "Unable to call app for restore: " + packageName, e); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, e.toString()); - agentErrorCleanup(); // clears any pending timeout messages as well - - // After a restore failure we go back to running the queue. If there - // are no more packages to be restored that will be handled by the - // next step. - executeNextState(RestoreState.RUNNING_QUEUE); - } - } - - void agentErrorCleanup() { - // If the agent fails restore, it might have put the app's data - // into an incoherent state. For consistency we wipe its data - // again in this case before continuing with normal teardown - clearApplicationDataSynchronous(mCurrentPackage.packageName); - agentCleanup(); - } - - void agentCleanup() { - mBackupDataName.delete(); - try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} - try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} - mBackupData = mNewState = null; - - // if everything went okay, remember the recorded state now - // - // !!! TODO: the restored data should be migrated on the server - // side into the current dataset. In that case the new state file - // we just created would reflect the data already extant in the - // backend, so there'd be nothing more to do. Until that happens, - // however, we need to make sure that we record the data to the - // current backend dataset. (Yes, this means shipping the data over - // the wire in both directions. That's bad, but consistency comes - // first, then efficiency.) Once we introduce server-side data - // migration to the newly-restored device's dataset, we will change - // the following from a discard of the newly-written state to the - // "correct" operation of renaming into the canonical state blob. - mNewStateName.delete(); // TODO: remove; see above comment - //mNewStateName.renameTo(mSavedStateName); // TODO: replace with this - - // If this wasn't the PM pseudopackage, tear down the agent side - if (mCurrentPackage.applicationInfo != null) { - // unbind and tidy up even on timeout or failure - try { - mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); - - // The agent was probably running with a stub Application object, - // which isn't a valid run mode for the main app logic. Shut - // down the app so that next time it's launched, it gets the - // usual full initialization. Note that this is only done for - // full-system restores: when a single app has requested a restore, - // it is explicitly not killed following that operation. - if (mTargetPackage == null && (mCurrentPackage.applicationInfo.flags - & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0) { - if (DEBUG) Slog.d(TAG, "Restore complete, killing host process of " - + mCurrentPackage.applicationInfo.processName); - mActivityManager.killApplicationProcess( - mCurrentPackage.applicationInfo.processName, - mCurrentPackage.applicationInfo.uid); - } - } catch (RemoteException e) { - // can't happen; we run in the same process as the activity manager - } - } - - // The caller is responsible for reestablishing the state machine; our - // responsibility here is to clear the decks for whatever comes next. - mBackupHandler.removeMessages(MSG_TIMEOUT, this); - synchronized (mCurrentOpLock) { - mCurrentOperations.clear(); - } - } - - // A call to agent.doRestore() has been positively acknowledged as complete - @Override - public void operationComplete() { - int size = (int) mBackupDataName.length(); - EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE, mCurrentPackage.packageName, size); - // Just go back to running the restore queue - agentCleanup(); - - executeNextState(RestoreState.RUNNING_QUEUE); - } - - // A call to agent.doRestore() has timed out - @Override - public void handleTimeout() { - Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, - mCurrentPackage.packageName, "restore timeout"); - // Handle like an agent that threw on invocation: wipe it and go on to the next - agentErrorCleanup(); - executeNextState(RestoreState.RUNNING_QUEUE); - } - - void executeNextState(RestoreState nextState) { - if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " - + this + " nextState=" + nextState); - mCurrentState = nextState; - Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); - mBackupHandler.sendMessage(msg); - } - } - - class PerformClearTask implements Runnable { - IBackupTransport mTransport; - PackageInfo mPackage; - - PerformClearTask(IBackupTransport transport, PackageInfo packageInfo) { - mTransport = transport; - mPackage = packageInfo; - } - - public void run() { - try { - // Clear the on-device backup state to ensure a full backup next time - File stateDir = new File(mBaseStateDir, mTransport.transportDirName()); - File stateFile = new File(stateDir, mPackage.packageName); - stateFile.delete(); - - // Tell the transport to remove all the persistent storage for the app - // TODO - need to handle failures - mTransport.clearBackupData(mPackage); - } catch (RemoteException e) { - // can't happen; the transport is local - } catch (Exception e) { - Slog.e(TAG, "Transport threw attempting to clear data for " + mPackage); - } finally { - try { - // TODO - need to handle failures - mTransport.finishBackup(); - } catch (RemoteException e) { - // can't happen; the transport is local - } - - // Last but not least, release the cpu - mWakelock.release(); - } - } - } - - class PerformInitializeTask implements Runnable { - HashSet mQueue; - - PerformInitializeTask(HashSet transportNames) { - mQueue = transportNames; - } - - public void run() { - try { - for (String transportName : mQueue) { - IBackupTransport transport = getTransport(transportName); - if (transport == null) { - Slog.e(TAG, "Requested init for " + transportName + " but not found"); - continue; - } - - Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName); - EventLog.writeEvent(EventLogTags.BACKUP_START, transport.transportDirName()); - long startRealtime = SystemClock.elapsedRealtime(); - int status = transport.initializeDevice(); - - if (status == BackupConstants.TRANSPORT_OK) { - status = transport.finishBackup(); - } - - // Okay, the wipe really happened. Clean up our local bookkeeping. - if (status == BackupConstants.TRANSPORT_OK) { - Slog.i(TAG, "Device init successful"); - int millis = (int) (SystemClock.elapsedRealtime() - startRealtime); - EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); - resetBackupState(new File(mBaseStateDir, transport.transportDirName())); - EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis); - synchronized (mQueueLock) { - recordInitPendingLocked(false, transportName); - } - } else { - // If this didn't work, requeue this one and try again - // after a suitable interval - Slog.e(TAG, "Transport error in initializeDevice()"); - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); - synchronized (mQueueLock) { - recordInitPendingLocked(true, transportName); - } - // do this via another alarm to make sure of the wakelock states - long delay = transport.requestBackupTime(); - if (DEBUG) Slog.w(TAG, "init failed on " - + transportName + " resched in " + delay); - mAlarmManager.set(AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + delay, mRunInitIntent); - } - } - } catch (RemoteException e) { - // can't happen; the transports are local - } catch (Exception e) { - Slog.e(TAG, "Unexpected error performing init", e); - } finally { - // Done; release the wakelock - mWakelock.release(); - } - } - } - - private void dataChangedImpl(String packageName) { - HashSet targets = dataChangedTargets(packageName); - dataChangedImpl(packageName, targets); - } - - private void dataChangedImpl(String packageName, HashSet targets) { - // Record that we need a backup pass for the caller. Since multiple callers - // may share a uid, we need to note all candidates within that uid and schedule - // a backup pass for each of them. - EventLog.writeEvent(EventLogTags.BACKUP_DATA_CHANGED, packageName); - - if (targets == null) { - Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" - + " uid=" + Binder.getCallingUid()); - return; - } - - synchronized (mQueueLock) { - // Note that this client has made data changes that need to be backed up - if (targets.contains(packageName)) { - // Add the caller to the set of pending backups. If there is - // one already there, then overwrite it, but no harm done. - BackupRequest req = new BackupRequest(packageName); - if (mPendingBackups.put(packageName, req) == null) { - if (DEBUG) Slog.d(TAG, "Now staging backup of " + packageName); - - // Journal this request in case of crash. The put() - // operation returned null when this package was not already - // in the set; we want to avoid touching the disk redundantly. - writeToJournalLocked(packageName); - - if (MORE_DEBUG) { - int numKeys = mPendingBackups.size(); - Slog.d(TAG, "Now awaiting backup for " + numKeys + " participants:"); - for (BackupRequest b : mPendingBackups.values()) { - Slog.d(TAG, " + " + b); - } - } - } - } - } - } - - // Note: packageName is currently unused, but may be in the future - private HashSet dataChangedTargets(String packageName) { - // If the caller does not hold the BACKUP permission, it can only request a - // backup of its own data. - if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), - Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { - synchronized (mBackupParticipants) { - return mBackupParticipants.get(Binder.getCallingUid()); - } - } - - // a caller with full permission can ask to back up any participating app - // !!! TODO: allow backup of ANY app? - HashSet targets = new HashSet(); - synchronized (mBackupParticipants) { - int N = mBackupParticipants.size(); - for (int i = 0; i < N; i++) { - HashSet s = mBackupParticipants.valueAt(i); - if (s != null) { - targets.addAll(s); - } - } - } - return targets; - } - - private void writeToJournalLocked(String str) { - RandomAccessFile out = null; - try { - if (mJournal == null) mJournal = File.createTempFile("journal", null, mJournalDir); - out = new RandomAccessFile(mJournal, "rws"); - out.seek(out.length()); - out.writeUTF(str); - } catch (IOException e) { - Slog.e(TAG, "Can't write " + str + " to backup journal", e); - mJournal = null; - } finally { - try { if (out != null) out.close(); } catch (IOException e) {} - } - } - - // ----- IBackupManager binder interface ----- - - public void dataChanged(final String packageName) { - final int callingUserHandle = UserHandle.getCallingUserId(); - if (callingUserHandle != UserHandle.USER_OWNER) { - // App is running under a non-owner user profile. For now, we do not back - // up data from secondary user profiles. - // TODO: backups for all user profiles. - if (MORE_DEBUG) { - Slog.v(TAG, "dataChanged(" + packageName + ") ignored because it's user " - + callingUserHandle); - } - return; - } - - final HashSet targets = dataChangedTargets(packageName); - if (targets == null) { - Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" - + " uid=" + Binder.getCallingUid()); - return; - } - - mBackupHandler.post(new Runnable() { - public void run() { - dataChangedImpl(packageName, targets); - } - }); - } - - // Clear the given package's backup data from the current transport - public void clearBackupData(String transportName, String packageName) { - if (DEBUG) Slog.v(TAG, "clearBackupData() of " + packageName + " on " + transportName); - PackageInfo info; - try { - info = mPackageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); - } catch (NameNotFoundException e) { - Slog.d(TAG, "No such package '" + packageName + "' - not clearing backup data"); - return; - } - - // If the caller does not hold the BACKUP permission, it can only request a - // wipe of its own backed-up data. - HashSet apps; - if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), - Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { - apps = mBackupParticipants.get(Binder.getCallingUid()); - } else { - // a caller with full permission can ask to back up any participating app - // !!! TODO: allow data-clear of ANY app? - if (DEBUG) Slog.v(TAG, "Privileged caller, allowing clear of other apps"); - apps = new HashSet(); - int N = mBackupParticipants.size(); - for (int i = 0; i < N; i++) { - HashSet s = mBackupParticipants.valueAt(i); - if (s != null) { - apps.addAll(s); - } - } - } - - // Is the given app an available participant? - if (apps.contains(packageName)) { - // found it; fire off the clear request - if (DEBUG) Slog.v(TAG, "Found the app - running clear process"); - mBackupHandler.removeMessages(MSG_RETRY_CLEAR); - synchronized (mQueueLock) { - final IBackupTransport transport = getTransport(transportName); - if (transport == null) { - // transport is currently unavailable -- make sure to retry - Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR, - new ClearRetryParams(transportName, packageName)); - mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL); - return; - } - long oldId = Binder.clearCallingIdentity(); - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_CLEAR, - new ClearParams(transport, info)); - mBackupHandler.sendMessage(msg); - Binder.restoreCallingIdentity(oldId); - } - } - } - - // Run a backup pass immediately for any applications that have declared - // that they have pending updates. - public void backupNow() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow"); - - if (DEBUG) Slog.v(TAG, "Scheduling immediate backup pass"); - synchronized (mQueueLock) { - // Because the alarms we are using can jitter, and we want an *immediate* - // backup pass to happen, we restart the timer beginning with "next time," - // then manually fire the backup trigger intent ourselves. - startBackupAlarmsLocked(BACKUP_INTERVAL); - try { - mRunBackupIntent.send(); - } catch (PendingIntent.CanceledException e) { - // should never happen - Slog.e(TAG, "run-backup intent cancelled!"); - } - } - } - - boolean deviceIsProvisioned() { - final ContentResolver resolver = mContext.getContentResolver(); - return (Settings.Global.getInt(resolver, Settings.Global.DEVICE_PROVISIONED, 0) != 0); - } - - // Run a *full* backup pass for the given package, writing the resulting data stream - // to the supplied file descriptor. This method is synchronous and does not return - // to the caller until the backup has been completed. - public void fullBackup(ParcelFileDescriptor fd, boolean includeApks, - boolean includeObbs, boolean includeShared, - boolean doAllApps, boolean includeSystem, String[] pkgList) { - mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup"); - - final int callingUserHandle = UserHandle.getCallingUserId(); - if (callingUserHandle != UserHandle.USER_OWNER) { - throw new IllegalStateException("Backup supported only for the device owner"); - } - - // Validate - if (!doAllApps) { - if (!includeShared) { - // If we're backing up shared data (sdcard or equivalent), then we can run - // without any supplied app names. Otherwise, we'd be doing no work, so - // report the error. - if (pkgList == null || pkgList.length == 0) { - throw new IllegalArgumentException( - "Backup requested but neither shared nor any apps named"); - } - } - } - - long oldId = Binder.clearCallingIdentity(); - try { - // Doesn't make sense to do a full backup prior to setup - if (!deviceIsProvisioned()) { - Slog.i(TAG, "Full backup not supported before setup"); - return; - } - - if (DEBUG) Slog.v(TAG, "Requesting full backup: apks=" + includeApks - + " obb=" + includeObbs + " shared=" + includeShared + " all=" + doAllApps - + " pkgs=" + pkgList); - Slog.i(TAG, "Beginning full backup..."); - - FullBackupParams params = new FullBackupParams(fd, includeApks, includeObbs, - includeShared, doAllApps, includeSystem, pkgList); - final int token = generateToken(); - synchronized (mFullConfirmations) { - mFullConfirmations.put(token, params); - } - - // start up the confirmation UI - if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token); - if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { - Slog.e(TAG, "Unable to launch full backup confirmation"); - mFullConfirmations.delete(token); - return; - } - - // make sure the screen is lit for the user interaction - mPowerManager.userActivity(SystemClock.uptimeMillis(), false); - - // start the confirmation countdown - startConfirmationTimeout(token, params); - - // wait for the backup to be performed - if (DEBUG) Slog.d(TAG, "Waiting for full backup completion..."); - waitForCompletion(params); - } finally { - try { - fd.close(); - } catch (IOException e) { - // just eat it - } - Binder.restoreCallingIdentity(oldId); - Slog.d(TAG, "Full backup processing complete."); - } - } - - public void fullRestore(ParcelFileDescriptor fd) { - mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullRestore"); - - final int callingUserHandle = UserHandle.getCallingUserId(); - if (callingUserHandle != UserHandle.USER_OWNER) { - throw new IllegalStateException("Restore supported only for the device owner"); - } - - long oldId = Binder.clearCallingIdentity(); - - try { - // Check whether the device has been provisioned -- we don't handle - // full restores prior to completing the setup process. - if (!deviceIsProvisioned()) { - Slog.i(TAG, "Full restore not permitted before setup"); - return; - } - - Slog.i(TAG, "Beginning full restore..."); - - FullRestoreParams params = new FullRestoreParams(fd); - final int token = generateToken(); - synchronized (mFullConfirmations) { - mFullConfirmations.put(token, params); - } - - // start up the confirmation UI - if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token); - if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { - Slog.e(TAG, "Unable to launch full restore confirmation"); - mFullConfirmations.delete(token); - return; - } - - // make sure the screen is lit for the user interaction - mPowerManager.userActivity(SystemClock.uptimeMillis(), false); - - // start the confirmation countdown - startConfirmationTimeout(token, params); - - // wait for the restore to be performed - if (DEBUG) Slog.d(TAG, "Waiting for full restore completion..."); - waitForCompletion(params); - } finally { - try { - fd.close(); - } catch (IOException e) { - Slog.w(TAG, "Error trying to close fd after full restore: " + e); - } - Binder.restoreCallingIdentity(oldId); - Slog.i(TAG, "Full restore processing complete."); - } - } - - boolean startConfirmationUi(int token, String action) { - try { - Intent confIntent = new Intent(action); - confIntent.setClassName("com.android.backupconfirm", - "com.android.backupconfirm.BackupRestoreConfirmation"); - confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); - confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(confIntent); - } catch (ActivityNotFoundException e) { - return false; - } - return true; - } - - void startConfirmationTimeout(int token, FullParams params) { - if (MORE_DEBUG) Slog.d(TAG, "Posting conf timeout msg after " - + TIMEOUT_FULL_CONFIRMATION + " millis"); - Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, - token, 0, params); - mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); - } - - void waitForCompletion(FullParams params) { - synchronized (params.latch) { - while (params.latch.get() == false) { - try { - params.latch.wait(); - } catch (InterruptedException e) { /* never interrupted */ } - } - } - } - - void signalFullBackupRestoreCompletion(FullParams params) { - synchronized (params.latch) { - params.latch.set(true); - params.latch.notifyAll(); - } - } - - // Confirm that the previously-requested full backup/restore operation can proceed. This - // is used to require a user-facing disclosure about the operation. - @Override - public void acknowledgeFullBackupOrRestore(int token, boolean allow, - String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { - if (DEBUG) Slog.d(TAG, "acknowledgeFullBackupOrRestore : token=" + token - + " allow=" + allow); - - // TODO: possibly require not just this signature-only permission, but even - // require that the specific designated confirmation-UI app uid is the caller? - mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "acknowledgeFullBackupOrRestore"); - - long oldId = Binder.clearCallingIdentity(); - try { - - FullParams params; - synchronized (mFullConfirmations) { - params = mFullConfirmations.get(token); - if (params != null) { - mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params); - mFullConfirmations.delete(token); - - if (allow) { - final int verb = params instanceof FullBackupParams - ? MSG_RUN_FULL_BACKUP - : MSG_RUN_FULL_RESTORE; - - params.observer = observer; - params.curPassword = curPassword; - - boolean isEncrypted; - try { - isEncrypted = (mMountService.getEncryptionState() != MountService.ENCRYPTION_STATE_NONE); - if (isEncrypted) Slog.w(TAG, "Device is encrypted; forcing enc password"); - } catch (RemoteException e) { - // couldn't contact the mount service; fail "safe" and assume encryption - Slog.e(TAG, "Unable to contact mount service!"); - isEncrypted = true; - } - params.encryptPassword = (isEncrypted) ? curPassword : encPpassword; - - if (DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb); - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(verb, params); - mBackupHandler.sendMessage(msg); - } else { - Slog.w(TAG, "User rejected full backup/restore operation"); - // indicate completion without having actually transferred any data - signalFullBackupRestoreCompletion(params); - } - } else { - Slog.w(TAG, "Attempted to ack full backup/restore with invalid token"); - } - } - } finally { - Binder.restoreCallingIdentity(oldId); - } - } - - // Enable/disable the backup service - @Override - public void setBackupEnabled(boolean enable) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "setBackupEnabled"); - - Slog.i(TAG, "Backup enabled => " + enable); - - long oldId = Binder.clearCallingIdentity(); - try { - boolean wasEnabled = mEnabled; - synchronized (this) { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.BACKUP_ENABLED, enable ? 1 : 0); - mEnabled = enable; - } - - synchronized (mQueueLock) { - if (enable && !wasEnabled && mProvisioned) { - // if we've just been enabled, start scheduling backup passes - startBackupAlarmsLocked(BACKUP_INTERVAL); - } else if (!enable) { - // No longer enabled, so stop running backups - if (DEBUG) Slog.i(TAG, "Opting out of backup"); - - mAlarmManager.cancel(mRunBackupIntent); - - // This also constitutes an opt-out, so we wipe any data for - // this device from the backend. We start that process with - // an alarm in order to guarantee wakelock states. - if (wasEnabled && mProvisioned) { - // NOTE: we currently flush every registered transport, not just - // the currently-active one. - HashSet allTransports; - synchronized (mTransports) { - allTransports = new HashSet(mTransports.keySet()); - } - // build the set of transports for which we are posting an init - for (String transport : allTransports) { - recordInitPendingLocked(true, transport); - } - mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), - mRunInitIntent); - } - } - } - } finally { - Binder.restoreCallingIdentity(oldId); - } - } - - // Enable/disable automatic restore of app data at install time - public void setAutoRestore(boolean doAutoRestore) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "setAutoRestore"); - - Slog.i(TAG, "Auto restore => " + doAutoRestore); - - synchronized (this) { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0); - mAutoRestore = doAutoRestore; - } - } - - // Mark the backup service as having been provisioned - public void setBackupProvisioned(boolean available) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "setBackupProvisioned"); - /* - * This is now a no-op; provisioning is simply the device's own setup state. - */ - } - - private void startBackupAlarmsLocked(long delayBeforeFirstBackup) { - // We used to use setInexactRepeating(), but that may be linked to - // backups running at :00 more often than not, creating load spikes. - // Schedule at an exact time for now, and also add a bit of "fuzz". - - Random random = new Random(); - long when = System.currentTimeMillis() + delayBeforeFirstBackup + - random.nextInt(FUZZ_MILLIS); - mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, when, - BACKUP_INTERVAL + random.nextInt(FUZZ_MILLIS), mRunBackupIntent); - mNextBackupPass = when; - } - - // Report whether the backup mechanism is currently enabled - public boolean isBackupEnabled() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "isBackupEnabled"); - return mEnabled; // no need to synchronize just to read it - } - - // Report the name of the currently active transport - public String getCurrentTransport() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "getCurrentTransport"); - if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + mCurrentTransport); - return mCurrentTransport; - } - - // Report all known, available backup transports - public String[] listAllTransports() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "listAllTransports"); - - String[] list = null; - ArrayList known = new ArrayList(); - for (Map.Entry entry : mTransports.entrySet()) { - if (entry.getValue() != null) { - known.add(entry.getKey()); - } - } - - if (known.size() > 0) { - list = new String[known.size()]; - known.toArray(list); - } - return list; - } - - // Select which transport to use for the next backup operation. If the given - // name is not one of the available transports, no action is taken and the method - // returns null. - public String selectBackupTransport(String transport) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "selectBackupTransport"); - - synchronized (mTransports) { - String prevTransport = null; - if (mTransports.get(transport) != null) { - prevTransport = mCurrentTransport; - mCurrentTransport = transport; - Settings.Secure.putString(mContext.getContentResolver(), - Settings.Secure.BACKUP_TRANSPORT, transport); - Slog.v(TAG, "selectBackupTransport() set " + mCurrentTransport - + " returning " + prevTransport); - } else { - Slog.w(TAG, "Attempt to select unavailable transport " + transport); - } - return prevTransport; - } - } - - // Supply the configuration Intent for the given transport. If the name is not one - // of the available transports, or if the transport does not supply any configuration - // UI, the method returns null. - public Intent getConfigurationIntent(String transportName) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "getConfigurationIntent"); - - synchronized (mTransports) { - final IBackupTransport transport = mTransports.get(transportName); - if (transport != null) { - try { - final Intent intent = transport.configurationIntent(); - if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent " - + intent); - return intent; - } catch (RemoteException e) { - /* fall through to return null */ - } - } - } - - return null; - } - - // Supply the configuration summary string for the given transport. If the name is - // not one of the available transports, or if the transport does not supply any - // summary / destination string, the method can return null. - // - // This string is used VERBATIM as the summary text of the relevant Settings item! - public String getDestinationString(String transportName) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "getDestinationString"); - - synchronized (mTransports) { - final IBackupTransport transport = mTransports.get(transportName); - if (transport != null) { - try { - final String text = transport.currentDestinationString(); - if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text); - return text; - } catch (RemoteException e) { - /* fall through to return null */ - } - } - } - - return null; - } - - // Callback: a requested backup agent has been instantiated. This should only - // be called from the Activity Manager. - public void agentConnected(String packageName, IBinder agentBinder) { - synchronized(mAgentConnectLock) { - if (Binder.getCallingUid() == Process.SYSTEM_UID) { - Slog.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); - IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder); - mConnectedAgent = agent; - mConnecting = false; - } else { - Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() - + " claiming agent connected"); - } - mAgentConnectLock.notifyAll(); - } - } - - // Callback: a backup agent has failed to come up, or has unexpectedly quit. - // If the agent failed to come up in the first place, the agentBinder argument - // will be null. This should only be called from the Activity Manager. - public void agentDisconnected(String packageName) { - // TODO: handle backup being interrupted - synchronized(mAgentConnectLock) { - if (Binder.getCallingUid() == Process.SYSTEM_UID) { - mConnectedAgent = null; - mConnecting = false; - } else { - Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() - + " claiming agent disconnected"); - } - mAgentConnectLock.notifyAll(); - } - } - - // An application being installed will need a restore pass, then the Package Manager - // will need to be told when the restore is finished. - public void restoreAtInstall(String packageName, int token) { - if (Binder.getCallingUid() != Process.SYSTEM_UID) { - Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() - + " attemping install-time restore"); - return; - } - - long restoreSet = getAvailableRestoreToken(packageName); - if (DEBUG) Slog.v(TAG, "restoreAtInstall pkg=" + packageName - + " token=" + Integer.toHexString(token) - + " restoreSet=" + Long.toHexString(restoreSet)); - - if (mAutoRestore && mProvisioned && restoreSet != 0) { - // Do we have a transport to fetch data for us? - IBackupTransport transport = getTransport(mCurrentTransport); - if (transport == null) { - if (DEBUG) Slog.w(TAG, "No transport for install-time restore"); - return; - } - - try { - // okay, we're going to attempt a restore of this package from this restore set. - // The eventual message back into the Package Manager to run the post-install - // steps for 'token' will be issued from the restore handling code. - - // This can throw and so *must* happen before the wakelock is acquired - String dirName = transport.transportDirName(); - - // We can use a synthetic PackageInfo here because: - // 1. We know it's valid, since the Package Manager supplied the name - // 2. Only the packageName field will be used by the restore code - PackageInfo pkg = new PackageInfo(); - pkg.packageName = packageName; - - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); - msg.obj = new RestoreParams(transport, dirName, null, - restoreSet, pkg, token, true); - mBackupHandler.sendMessage(msg); - } catch (RemoteException e) { - // Binding to the transport broke; back off and proceed with the installation. - Slog.e(TAG, "Unable to contact transport for install-time restore"); - } - } else { - // Auto-restore disabled or no way to attempt a restore; just tell the Package - // Manager to proceed with the post-install handling for this package. - if (DEBUG) Slog.v(TAG, "No restore set -- skipping restore"); - try { - mPackageManagerBinder.finishPackageInstall(token); - } catch (RemoteException e) { /* can't happen */ } - } - } - - // Hand off a restore session - public IRestoreSession beginRestoreSession(String packageName, String transport) { - if (DEBUG) Slog.v(TAG, "beginRestoreSession: pkg=" + packageName - + " transport=" + transport); - - boolean needPermission = true; - if (transport == null) { - transport = mCurrentTransport; - - if (packageName != null) { - PackageInfo app = null; - try { - app = mPackageManager.getPackageInfo(packageName, 0); - } catch (NameNotFoundException nnf) { - Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); - throw new IllegalArgumentException("Package " + packageName + " not found"); - } - - if (app.applicationInfo.uid == Binder.getCallingUid()) { - // So: using the current active transport, and the caller has asked - // that its own package will be restored. In this narrow use case - // we do not require the caller to hold the permission. - needPermission = false; - } - } - } - - if (needPermission) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "beginRestoreSession"); - } else { - if (DEBUG) Slog.d(TAG, "restoring self on current transport; no permission needed"); - } - - synchronized(this) { - if (mActiveRestoreSession != null) { - Slog.d(TAG, "Restore session requested but one already active"); - return null; - } - mActiveRestoreSession = new ActiveRestoreSession(packageName, transport); - mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); - } - return mActiveRestoreSession; - } - - void clearRestoreSession(ActiveRestoreSession currentSession) { - synchronized(this) { - if (currentSession != mActiveRestoreSession) { - Slog.e(TAG, "ending non-current restore session"); - } else { - if (DEBUG) Slog.v(TAG, "Clearing restore session and halting timeout"); - mActiveRestoreSession = null; - mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); - } - } - } - - // Note that a currently-active backup agent has notified us that it has - // completed the given outstanding asynchronous backup/restore operation. - @Override - public void opComplete(int token) { - if (MORE_DEBUG) Slog.v(TAG, "opComplete: " + Integer.toHexString(token)); - Operation op = null; - synchronized (mCurrentOpLock) { - op = mCurrentOperations.get(token); - if (op != null) { - op.state = OP_ACKNOWLEDGED; - } - mCurrentOpLock.notifyAll(); - } - - // The completion callback, if any, is invoked on the handler - if (op != null && op.callback != null) { - Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, op.callback); - mBackupHandler.sendMessage(msg); - } - } - - // ----- Restore session ----- - - class ActiveRestoreSession extends IRestoreSession.Stub { - private static final String TAG = "RestoreSession"; - - private String mPackageName; - private IBackupTransport mRestoreTransport = null; - RestoreSet[] mRestoreSets = null; - boolean mEnded = false; - - ActiveRestoreSession(String packageName, String transport) { - mPackageName = packageName; - mRestoreTransport = getTransport(transport); - } - - // --- Binder interface --- - public synchronized int getAvailableRestoreSets(IRestoreObserver observer) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "getAvailableRestoreSets"); - if (observer == null) { - throw new IllegalArgumentException("Observer must not be null"); - } - - if (mEnded) { - throw new IllegalStateException("Restore session already ended"); - } - - long oldId = Binder.clearCallingIdentity(); - try { - if (mRestoreTransport == null) { - Slog.w(TAG, "Null transport getting restore sets"); - return -1; - } - // spin off the transport request to our service thread - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_GET_RESTORE_SETS, - new RestoreGetSetsParams(mRestoreTransport, this, observer)); - mBackupHandler.sendMessage(msg); - return 0; - } catch (Exception e) { - Slog.e(TAG, "Error in getAvailableRestoreSets", e); - return -1; - } finally { - Binder.restoreCallingIdentity(oldId); - } - } - - public synchronized int restoreAll(long token, IRestoreObserver observer) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "performRestore"); - - if (DEBUG) Slog.d(TAG, "restoreAll token=" + Long.toHexString(token) - + " observer=" + observer); - - if (mEnded) { - throw new IllegalStateException("Restore session already ended"); - } - - if (mRestoreTransport == null || mRestoreSets == null) { - Slog.e(TAG, "Ignoring restoreAll() with no restore set"); - return -1; - } - - if (mPackageName != null) { - Slog.e(TAG, "Ignoring restoreAll() on single-package session"); - return -1; - } - - String dirName; - try { - dirName = mRestoreTransport.transportDirName(); - } catch (RemoteException e) { - // Transport went AWOL; fail. - Slog.e(TAG, "Unable to contact transport for restore"); - return -1; - } - - synchronized (mQueueLock) { - for (int i = 0; i < mRestoreSets.length; i++) { - if (token == mRestoreSets[i].token) { - long oldId = Binder.clearCallingIdentity(); - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); - msg.obj = new RestoreParams(mRestoreTransport, dirName, - observer, token, true); - mBackupHandler.sendMessage(msg); - Binder.restoreCallingIdentity(oldId); - return 0; - } - } - } - - Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); - return -1; - } - - public synchronized int restoreSome(long token, IRestoreObserver observer, - String[] packages) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, - "performRestore"); - - if (DEBUG) { - StringBuilder b = new StringBuilder(128); - b.append("restoreSome token="); - b.append(Long.toHexString(token)); - b.append(" observer="); - b.append(observer.toString()); - b.append(" packages="); - if (packages == null) { - b.append("null"); - } else { - b.append('{'); - boolean first = true; - for (String s : packages) { - if (!first) { - b.append(", "); - } else first = false; - b.append(s); - } - b.append('}'); - } - Slog.d(TAG, b.toString()); - } - - if (mEnded) { - throw new IllegalStateException("Restore session already ended"); - } - - if (mRestoreTransport == null || mRestoreSets == null) { - Slog.e(TAG, "Ignoring restoreAll() with no restore set"); - return -1; - } - - if (mPackageName != null) { - Slog.e(TAG, "Ignoring restoreAll() on single-package session"); - return -1; - } - - String dirName; - try { - dirName = mRestoreTransport.transportDirName(); - } catch (RemoteException e) { - // Transport went AWOL; fail. - Slog.e(TAG, "Unable to contact transport for restore"); - return -1; - } - - synchronized (mQueueLock) { - for (int i = 0; i < mRestoreSets.length; i++) { - if (token == mRestoreSets[i].token) { - long oldId = Binder.clearCallingIdentity(); - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); - msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, token, - packages, true); - mBackupHandler.sendMessage(msg); - Binder.restoreCallingIdentity(oldId); - return 0; - } - } - } - - Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); - return -1; - } - - public synchronized int restorePackage(String packageName, IRestoreObserver observer) { - if (DEBUG) Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer); - - if (mEnded) { - throw new IllegalStateException("Restore session already ended"); - } - - if (mPackageName != null) { - if (! mPackageName.equals(packageName)) { - Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName - + " on session for package " + mPackageName); - return -1; - } - } - - PackageInfo app = null; - try { - app = mPackageManager.getPackageInfo(packageName, 0); - } catch (NameNotFoundException nnf) { - Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); - return -1; - } - - // If the caller is not privileged and is not coming from the target - // app's uid, throw a permission exception back to the caller. - int perm = mContext.checkPermission(android.Manifest.permission.BACKUP, - Binder.getCallingPid(), Binder.getCallingUid()); - if ((perm == PackageManager.PERMISSION_DENIED) && - (app.applicationInfo.uid != Binder.getCallingUid())) { - Slog.w(TAG, "restorePackage: bad packageName=" + packageName - + " or calling uid=" + Binder.getCallingUid()); - throw new SecurityException("No permission to restore other packages"); - } - - // If the package has no backup agent, we obviously cannot proceed - if (app.applicationInfo.backupAgentName == null) { - Slog.w(TAG, "Asked to restore package " + packageName + " with no agent"); - return -1; - } - - // So far so good; we're allowed to try to restore this package. Now - // check whether there is data for it in the current dataset, falling back - // to the ancestral dataset if not. - long token = getAvailableRestoreToken(packageName); - - // If we didn't come up with a place to look -- no ancestral dataset and - // the app has never been backed up from this device -- there's nothing - // to do but return failure. - if (token == 0) { - if (DEBUG) Slog.w(TAG, "No data available for this package; not restoring"); - return -1; - } - - String dirName; - try { - dirName = mRestoreTransport.transportDirName(); - } catch (RemoteException e) { - // Transport went AWOL; fail. - Slog.e(TAG, "Unable to contact transport for restore"); - return -1; - } - - // Ready to go: enqueue the restore request and claim success - long oldId = Binder.clearCallingIdentity(); - mWakelock.acquire(); - Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); - msg.obj = new RestoreParams(mRestoreTransport, dirName, - observer, token, app, 0, false); - mBackupHandler.sendMessage(msg); - Binder.restoreCallingIdentity(oldId); - return 0; - } - - // Posted to the handler to tear down a restore session in a cleanly synchronized way - class EndRestoreRunnable implements Runnable { - BackupManagerService mBackupManager; - ActiveRestoreSession mSession; - - EndRestoreRunnable(BackupManagerService manager, ActiveRestoreSession session) { - mBackupManager = manager; - mSession = session; - } - - public void run() { - // clean up the session's bookkeeping - synchronized (mSession) { - try { - if (mSession.mRestoreTransport != null) { - mSession.mRestoreTransport.finishRestore(); - } - } catch (Exception e) { - Slog.e(TAG, "Error in finishRestore", e); - } finally { - mSession.mRestoreTransport = null; - mSession.mEnded = true; - } - } - - // clean up the BackupManagerService side of the bookkeeping - // and cancel any pending timeout message - mBackupManager.clearRestoreSession(mSession); - } - } - - public synchronized void endRestoreSession() { - if (DEBUG) Slog.d(TAG, "endRestoreSession"); - - if (mEnded) { - throw new IllegalStateException("Restore session already ended"); - } - - mBackupHandler.post(new EndRestoreRunnable(BackupManagerService.this, this)); - } - } - - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); - - long identityToken = Binder.clearCallingIdentity(); - try { - dumpInternal(pw); - } finally { - Binder.restoreCallingIdentity(identityToken); - } - } - - private void dumpInternal(PrintWriter pw) { - synchronized (mQueueLock) { - pw.println("Backup Manager is " + (mEnabled ? "enabled" : "disabled") - + " / " + (!mProvisioned ? "not " : "") + "provisioned / " - + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init"); - pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled")); - if (mBackupRunning) pw.println("Backup currently running"); - pw.println("Last backup pass started: " + mLastBackupPass - + " (now = " + System.currentTimeMillis() + ')'); - pw.println(" next scheduled: " + mNextBackupPass); - - pw.println("Available transports:"); - for (String t : listAllTransports()) { - pw.println((t.equals(mCurrentTransport) ? " * " : " ") + t); - try { - IBackupTransport transport = getTransport(t); - File dir = new File(mBaseStateDir, transport.transportDirName()); - pw.println(" destination: " + transport.currentDestinationString()); - pw.println(" intent: " + transport.configurationIntent()); - for (File f : dir.listFiles()) { - pw.println(" " + f.getName() + " - " + f.length() + " state bytes"); - } - } catch (Exception e) { - Slog.e(TAG, "Error in transport", e); - pw.println(" Error: " + e); - } - } - - pw.println("Pending init: " + mPendingInits.size()); - for (String s : mPendingInits) { - pw.println(" " + s); - } - - if (DEBUG_BACKUP_TRACE) { - synchronized (mBackupTrace) { - if (!mBackupTrace.isEmpty()) { - pw.println("Most recent backup trace:"); - for (String s : mBackupTrace) { - pw.println(" " + s); - } - } - } - } - - int N = mBackupParticipants.size(); - pw.println("Participants:"); - for (int i=0; i participants = mBackupParticipants.valueAt(i); - for (String app: participants) { - pw.println(" " + app); - } - } - - pw.println("Ancestral packages: " - + (mAncestralPackages == null ? "none" : mAncestralPackages.size())); - if (mAncestralPackages != null) { - for (String pkg : mAncestralPackages) { - pw.println(" " + pkg); - } - } - - pw.println("Ever backed up: " + mEverStoredApps.size()); - for (String pkg : mEverStoredApps) { - pw.println(" " + pkg); - } - - pw.println("Pending backup: " + mPendingBackups.size()); - for (BackupRequest req : mPendingBackups.values()) { - pw.println(" " + req); - } - } - } -} diff --git a/services/java/com/android/server/ClipboardService.java b/services/java/com/android/server/ClipboardService.java deleted file mode 100644 index 069ae23f5596..000000000000 --- a/services/java/com/android/server/ClipboardService.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import android.app.ActivityManagerNative; -import android.app.AppGlobals; -import android.app.AppOpsManager; -import android.app.IActivityManager; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.IClipboard; -import android.content.IOnPrimaryClipChangedListener; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.Uri; -import android.os.Binder; -import android.os.IBinder; -import android.os.Parcel; -import android.os.Process; -import android.os.RemoteCallbackList; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.Pair; -import android.util.Slog; -import android.util.SparseArray; - -import java.util.HashSet; - -/** - * Implementation of the clipboard for copy and paste. - */ -public class ClipboardService extends IClipboard.Stub { - - private static final String TAG = "ClipboardService"; - - private final Context mContext; - private final IActivityManager mAm; - private final PackageManager mPm; - private final AppOpsManager mAppOps; - private final IBinder mPermissionOwner; - - private class ListenerInfo { - final int mUid; - final String mPackageName; - ListenerInfo(int uid, String packageName) { - mUid = uid; - mPackageName = packageName; - } - } - - private class PerUserClipboard { - final int userId; - - final RemoteCallbackList primaryClipListeners - = new RemoteCallbackList(); - - ClipData primaryClip; - - final HashSet activePermissionOwners - = new HashSet(); - - PerUserClipboard(int userId) { - this.userId = userId; - } - } - - private SparseArray mClipboards = new SparseArray(); - - /** - * Instantiates the clipboard. - */ - public ClipboardService(Context context) { - mContext = context; - mAm = ActivityManagerNative.getDefault(); - mPm = context.getPackageManager(); - mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); - IBinder permOwner = null; - try { - permOwner = mAm.newUriPermissionOwner("clipboard"); - } catch (RemoteException e) { - Slog.w("clipboard", "AM dead", e); - } - mPermissionOwner = permOwner; - - // Remove the clipboard if a user is removed - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_REMOVED); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_USER_REMOVED.equals(action)) { - removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); - } - } - }, userFilter); - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) - throws RemoteException { - try { - return super.onTransact(code, data, reply, flags); - } catch (RuntimeException e) { - if (!(e instanceof SecurityException)) { - Slog.wtf("clipboard", "Exception: ", e); - } - throw e; - } - - } - - private PerUserClipboard getClipboard() { - return getClipboard(UserHandle.getCallingUserId()); - } - - private PerUserClipboard getClipboard(int userId) { - synchronized (mClipboards) { - PerUserClipboard puc = mClipboards.get(userId); - if (puc == null) { - puc = new PerUserClipboard(userId); - mClipboards.put(userId, puc); - } - return puc; - } - } - - private void removeClipboard(int userId) { - synchronized (mClipboards) { - mClipboards.remove(userId); - } - } - - public void setPrimaryClip(ClipData clip, String callingPackage) { - synchronized (this) { - if (clip != null && clip.getItemCount() <= 0) { - throw new IllegalArgumentException("No items"); - } - final int callingUid = Binder.getCallingUid(); - if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return; - } - checkDataOwnerLocked(clip, callingUid); - clearActiveOwnersLocked(); - PerUserClipboard clipboard = getClipboard(); - clipboard.primaryClip = clip; - final long ident = Binder.clearCallingIdentity(); - final int n = clipboard.primaryClipListeners.beginBroadcast(); - try { - for (int i = 0; i < n; i++) { - try { - ListenerInfo li = (ListenerInfo) - clipboard.primaryClipListeners.getBroadcastCookie(i); - if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid, - li.mPackageName) == AppOpsManager.MODE_ALLOWED) { - clipboard.primaryClipListeners.getBroadcastItem(i) - .dispatchPrimaryClipChanged(); - } - } catch (RemoteException e) { - // The RemoteCallbackList will take care of removing - // the dead object for us. - } - } - } finally { - clipboard.primaryClipListeners.finishBroadcast(); - Binder.restoreCallingIdentity(ident); - } - } - } - - public ClipData getPrimaryClip(String pkg) { - synchronized (this) { - if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - pkg) != AppOpsManager.MODE_ALLOWED) { - return null; - } - addActiveOwnerLocked(Binder.getCallingUid(), pkg); - return getClipboard().primaryClip; - } - } - - public ClipDescription getPrimaryClipDescription(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return null; - } - PerUserClipboard clipboard = getClipboard(); - return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; - } - } - - public boolean hasPrimaryClip(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return false; - } - return getClipboard().primaryClip != null; - } - } - - public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, - String callingPackage) { - synchronized (this) { - getClipboard().primaryClipListeners.register(listener, - new ListenerInfo(Binder.getCallingUid(), callingPackage)); - } - } - - public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - synchronized (this) { - getClipboard().primaryClipListeners.unregister(listener); - } - } - - public boolean hasClipboardText(String callingPackage) { - synchronized (this) { - if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), - callingPackage) != AppOpsManager.MODE_ALLOWED) { - return false; - } - PerUserClipboard clipboard = getClipboard(); - if (clipboard.primaryClip != null) { - CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); - return text != null && text.length() > 0; - } - return false; - } - } - - private final void checkUriOwnerLocked(Uri uri, int uid) { - if (!"content".equals(uri.getScheme())) { - return; - } - long ident = Binder.clearCallingIdentity(); - try { - // This will throw SecurityException for us. - mAm.checkGrantUriPermission(uid, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - private final void checkItemOwnerLocked(ClipData.Item item, int uid) { - if (item.getUri() != null) { - checkUriOwnerLocked(item.getUri(), uid); - } - Intent intent = item.getIntent(); - if (intent != null && intent.getData() != null) { - checkUriOwnerLocked(intent.getData(), uid); - } - } - - private final void checkDataOwnerLocked(ClipData data, int uid) { - final int N = data.getItemCount(); - for (int i=0; i mAdminMap - = new HashMap(); - final ArrayList mAdminList - = new ArrayList(); - - public DevicePolicyData(int userHandle) { - mUserHandle = userHandle; - } - } - - final SparseArray mUserData = new SparseArray(); - - Handler mHandler = new Handler(); - - BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - getSendingUserId()); - if (Intent.ACTION_BOOT_COMPLETED.equals(action) - || ACTION_EXPIRED_PASSWORD_NOTIFICATION.equals(action)) { - if (DBG) Slog.v(TAG, "Sending password expiration notifications for action " - + action + " for user " + userHandle); - mHandler.post(new Runnable() { - public void run() { - handlePasswordExpirationNotification(getUserData(userHandle)); - } - }); - } - if (Intent.ACTION_BOOT_COMPLETED.equals(action) - || KeyChain.ACTION_STORAGE_CHANGED.equals(action)) { - manageMonitoringCertificateNotification(intent); - } - if (Intent.ACTION_USER_REMOVED.equals(action)) { - removeUserData(userHandle); - } else if (Intent.ACTION_USER_STARTED.equals(action) - || Intent.ACTION_PACKAGE_CHANGED.equals(action) - || Intent.ACTION_PACKAGE_REMOVED.equals(action) - || Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { - - if (Intent.ACTION_USER_STARTED.equals(action)) { - // Reset the policy data - synchronized (DevicePolicyManagerService.this) { - mUserData.remove(userHandle); - } - } - - handlePackagesChanged(userHandle); - } - } - }; - - static class ActiveAdmin { - final DeviceAdminInfo info; - - int passwordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - - static final int DEF_MINIMUM_PASSWORD_LENGTH = 0; - int minimumPasswordLength = DEF_MINIMUM_PASSWORD_LENGTH; - - static final int DEF_PASSWORD_HISTORY_LENGTH = 0; - int passwordHistoryLength = DEF_PASSWORD_HISTORY_LENGTH; - - static final int DEF_MINIMUM_PASSWORD_UPPER_CASE = 0; - int minimumPasswordUpperCase = DEF_MINIMUM_PASSWORD_UPPER_CASE; - - static final int DEF_MINIMUM_PASSWORD_LOWER_CASE = 0; - int minimumPasswordLowerCase = DEF_MINIMUM_PASSWORD_LOWER_CASE; - - static final int DEF_MINIMUM_PASSWORD_LETTERS = 1; - int minimumPasswordLetters = DEF_MINIMUM_PASSWORD_LETTERS; - - static final int DEF_MINIMUM_PASSWORD_NUMERIC = 1; - int minimumPasswordNumeric = DEF_MINIMUM_PASSWORD_NUMERIC; - - static final int DEF_MINIMUM_PASSWORD_SYMBOLS = 1; - int minimumPasswordSymbols = DEF_MINIMUM_PASSWORD_SYMBOLS; - - static final int DEF_MINIMUM_PASSWORD_NON_LETTER = 0; - int minimumPasswordNonLetter = DEF_MINIMUM_PASSWORD_NON_LETTER; - - static final long DEF_MAXIMUM_TIME_TO_UNLOCK = 0; - long maximumTimeToUnlock = DEF_MAXIMUM_TIME_TO_UNLOCK; - - static final int DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE = 0; - int maximumFailedPasswordsForWipe = DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE; - - static final long DEF_PASSWORD_EXPIRATION_TIMEOUT = 0; - long passwordExpirationTimeout = DEF_PASSWORD_EXPIRATION_TIMEOUT; - - static final long DEF_PASSWORD_EXPIRATION_DATE = 0; - long passwordExpirationDate = DEF_PASSWORD_EXPIRATION_DATE; - - static final int DEF_KEYGUARD_FEATURES_DISABLED = 0; // none - int disabledKeyguardFeatures = DEF_KEYGUARD_FEATURES_DISABLED; - - boolean encryptionRequested = false; - boolean disableCamera = false; - - // TODO: review implementation decisions with frameworks team - boolean specifiesGlobalProxy = false; - String globalProxySpec = null; - String globalProxyExclusionList = null; - - ActiveAdmin(DeviceAdminInfo _info) { - info = _info; - } - - int getUid() { return info.getActivityInfo().applicationInfo.uid; } - - public UserHandle getUserHandle() { - return new UserHandle(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid)); - } - - void writeToXml(XmlSerializer out) - throws IllegalArgumentException, IllegalStateException, IOException { - out.startTag(null, "policies"); - info.writePoliciesToXml(out); - out.endTag(null, "policies"); - if (passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { - out.startTag(null, "password-quality"); - out.attribute(null, "value", Integer.toString(passwordQuality)); - out.endTag(null, "password-quality"); - if (minimumPasswordLength != DEF_MINIMUM_PASSWORD_LENGTH) { - out.startTag(null, "min-password-length"); - out.attribute(null, "value", Integer.toString(minimumPasswordLength)); - out.endTag(null, "min-password-length"); - } - if(passwordHistoryLength != DEF_PASSWORD_HISTORY_LENGTH) { - out.startTag(null, "password-history-length"); - out.attribute(null, "value", Integer.toString(passwordHistoryLength)); - out.endTag(null, "password-history-length"); - } - if (minimumPasswordUpperCase != DEF_MINIMUM_PASSWORD_UPPER_CASE) { - out.startTag(null, "min-password-uppercase"); - out.attribute(null, "value", Integer.toString(minimumPasswordUpperCase)); - out.endTag(null, "min-password-uppercase"); - } - if (minimumPasswordLowerCase != DEF_MINIMUM_PASSWORD_LOWER_CASE) { - out.startTag(null, "min-password-lowercase"); - out.attribute(null, "value", Integer.toString(minimumPasswordLowerCase)); - out.endTag(null, "min-password-lowercase"); - } - if (minimumPasswordLetters != DEF_MINIMUM_PASSWORD_LETTERS) { - out.startTag(null, "min-password-letters"); - out.attribute(null, "value", Integer.toString(minimumPasswordLetters)); - out.endTag(null, "min-password-letters"); - } - if (minimumPasswordNumeric != DEF_MINIMUM_PASSWORD_NUMERIC) { - out.startTag(null, "min-password-numeric"); - out.attribute(null, "value", Integer.toString(minimumPasswordNumeric)); - out.endTag(null, "min-password-numeric"); - } - if (minimumPasswordSymbols != DEF_MINIMUM_PASSWORD_SYMBOLS) { - out.startTag(null, "min-password-symbols"); - out.attribute(null, "value", Integer.toString(minimumPasswordSymbols)); - out.endTag(null, "min-password-symbols"); - } - if (minimumPasswordNonLetter > DEF_MINIMUM_PASSWORD_NON_LETTER) { - out.startTag(null, "min-password-nonletter"); - out.attribute(null, "value", Integer.toString(minimumPasswordNonLetter)); - out.endTag(null, "min-password-nonletter"); - } - } - if (maximumTimeToUnlock != DEF_MAXIMUM_TIME_TO_UNLOCK) { - out.startTag(null, "max-time-to-unlock"); - out.attribute(null, "value", Long.toString(maximumTimeToUnlock)); - out.endTag(null, "max-time-to-unlock"); - } - if (maximumFailedPasswordsForWipe != DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE) { - out.startTag(null, "max-failed-password-wipe"); - out.attribute(null, "value", Integer.toString(maximumFailedPasswordsForWipe)); - out.endTag(null, "max-failed-password-wipe"); - } - if (specifiesGlobalProxy) { - out.startTag(null, "specifies-global-proxy"); - out.attribute(null, "value", Boolean.toString(specifiesGlobalProxy)); - out.endTag(null, "specifies_global_proxy"); - if (globalProxySpec != null) { - out.startTag(null, "global-proxy-spec"); - out.attribute(null, "value", globalProxySpec); - out.endTag(null, "global-proxy-spec"); - } - if (globalProxyExclusionList != null) { - out.startTag(null, "global-proxy-exclusion-list"); - out.attribute(null, "value", globalProxyExclusionList); - out.endTag(null, "global-proxy-exclusion-list"); - } - } - if (passwordExpirationTimeout != DEF_PASSWORD_EXPIRATION_TIMEOUT) { - out.startTag(null, "password-expiration-timeout"); - out.attribute(null, "value", Long.toString(passwordExpirationTimeout)); - out.endTag(null, "password-expiration-timeout"); - } - if (passwordExpirationDate != DEF_PASSWORD_EXPIRATION_DATE) { - out.startTag(null, "password-expiration-date"); - out.attribute(null, "value", Long.toString(passwordExpirationDate)); - out.endTag(null, "password-expiration-date"); - } - if (encryptionRequested) { - out.startTag(null, "encryption-requested"); - out.attribute(null, "value", Boolean.toString(encryptionRequested)); - out.endTag(null, "encryption-requested"); - } - if (disableCamera) { - out.startTag(null, "disable-camera"); - out.attribute(null, "value", Boolean.toString(disableCamera)); - out.endTag(null, "disable-camera"); - } - if (disabledKeyguardFeatures != DEF_KEYGUARD_FEATURES_DISABLED) { - out.startTag(null, "disable-keyguard-features"); - out.attribute(null, "value", Integer.toString(disabledKeyguardFeatures)); - out.endTag(null, "disable-keyguard-features"); - } - } - - void readFromXml(XmlPullParser parser) - throws XmlPullParserException, IOException { - int outerDepth = parser.getDepth(); - int type; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - String tag = parser.getName(); - if ("policies".equals(tag)) { - info.readPoliciesFromXml(parser); - } else if ("password-quality".equals(tag)) { - passwordQuality = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-length".equals(tag)) { - minimumPasswordLength = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("password-history-length".equals(tag)) { - passwordHistoryLength = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-uppercase".equals(tag)) { - minimumPasswordUpperCase = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-lowercase".equals(tag)) { - minimumPasswordLowerCase = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-letters".equals(tag)) { - minimumPasswordLetters = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-numeric".equals(tag)) { - minimumPasswordNumeric = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-symbols".equals(tag)) { - minimumPasswordSymbols = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("min-password-nonletter".equals(tag)) { - minimumPasswordNonLetter = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("max-time-to-unlock".equals(tag)) { - maximumTimeToUnlock = Long.parseLong( - parser.getAttributeValue(null, "value")); - } else if ("max-failed-password-wipe".equals(tag)) { - maximumFailedPasswordsForWipe = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else if ("specifies-global-proxy".equals(tag)) { - specifiesGlobalProxy = Boolean.parseBoolean( - parser.getAttributeValue(null, "value")); - } else if ("global-proxy-spec".equals(tag)) { - globalProxySpec = - parser.getAttributeValue(null, "value"); - } else if ("global-proxy-exclusion-list".equals(tag)) { - globalProxyExclusionList = - parser.getAttributeValue(null, "value"); - } else if ("password-expiration-timeout".equals(tag)) { - passwordExpirationTimeout = Long.parseLong( - parser.getAttributeValue(null, "value")); - } else if ("password-expiration-date".equals(tag)) { - passwordExpirationDate = Long.parseLong( - parser.getAttributeValue(null, "value")); - } else if ("encryption-requested".equals(tag)) { - encryptionRequested = Boolean.parseBoolean( - parser.getAttributeValue(null, "value")); - } else if ("disable-camera".equals(tag)) { - disableCamera = Boolean.parseBoolean( - parser.getAttributeValue(null, "value")); - } else if ("disable-keyguard-features".equals(tag)) { - disabledKeyguardFeatures = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } else { - Slog.w(TAG, "Unknown admin tag: " + tag); - } - XmlUtils.skipCurrentTag(parser); - } - } - - void dump(String prefix, PrintWriter pw) { - pw.print(prefix); pw.print("uid="); pw.println(getUid()); - pw.print(prefix); pw.println("policies:"); - ArrayList pols = info.getUsedPolicies(); - if (pols != null) { - for (int i=0; i= 0; i--) { - ActiveAdmin aa = policy.mAdminList.get(i); - try { - if (pm.getPackageInfo(aa.info.getPackageName(), 0, userHandle) == null - || pm.getReceiverInfo(aa.info.getComponent(), 0, userHandle) == null) { - removed = true; - policy.mAdminList.remove(i); - policy.mAdminMap.remove(aa.info.getComponent()); - } - } catch (RemoteException re) { - // Shouldn't happen - } - } - if (removed) { - validatePasswordOwnerLocked(policy); - syncDeviceCapabilitiesLocked(policy); - saveSettingsLocked(policy.mUserHandle); - } - } - - /** - * Instantiates the service. - */ - public DevicePolicyManagerService(Context context) { - mContext = context; - mHasFeature = context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_DEVICE_ADMIN); - mWakeLock = ((PowerManager)context.getSystemService(Context.POWER_SERVICE)) - .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DPM"); - if (!mHasFeature) { - // Skip the rest of the initialization - return; - } - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_BOOT_COMPLETED); - filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION); - filter.addAction(Intent.ACTION_USER_REMOVED); - filter.addAction(Intent.ACTION_USER_STARTED); - filter.addAction(KeyChain.ACTION_STORAGE_CHANGED); - context.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); - filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addDataScheme("package"); - context.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); - } - - /** - * Creates and loads the policy data from xml. - * @param userHandle the user for whom to load the policy data - * @return - */ - DevicePolicyData getUserData(int userHandle) { - synchronized (this) { - DevicePolicyData policy = mUserData.get(userHandle); - if (policy == null) { - policy = new DevicePolicyData(userHandle); - mUserData.append(userHandle, policy); - loadSettingsLocked(policy, userHandle); - } - return policy; - } - } - - void removeUserData(int userHandle) { - synchronized (this) { - if (userHandle == UserHandle.USER_OWNER) { - Slog.w(TAG, "Tried to remove device policy file for user 0! Ignoring."); - return; - } - DevicePolicyData policy = mUserData.get(userHandle); - if (policy != null) { - mUserData.remove(userHandle); - } - File policyFile = new File(Environment.getUserSystemDirectory(userHandle), - DEVICE_POLICIES_XML); - policyFile.delete(); - Slog.i(TAG, "Removed device policy file " + policyFile.getAbsolutePath()); - } - } - - void loadDeviceOwner() { - synchronized (this) { - if (DeviceOwner.isRegistered()) { - mDeviceOwner = new DeviceOwner(); - } - } - } - - /** - * Set an alarm for an upcoming event - expiration warning, expiration, or post-expiration - * reminders. Clears alarm if no expirations are configured. - */ - protected void setExpirationAlarmCheckLocked(Context context, DevicePolicyData policy) { - final long expiration = getPasswordExpirationLocked(null, policy.mUserHandle); - final long now = System.currentTimeMillis(); - final long timeToExpire = expiration - now; - final long alarmTime; - if (expiration == 0) { - // No expirations are currently configured: Cancel alarm. - alarmTime = 0; - } else if (timeToExpire <= 0) { - // The password has already expired: Repeat every 24 hours. - alarmTime = now + MS_PER_DAY; - } else { - // Selecting the next alarm time: Roll forward to the next 24 hour multiple before - // the expiration time. - long alarmInterval = timeToExpire % MS_PER_DAY; - if (alarmInterval == 0) { - alarmInterval = MS_PER_DAY; - } - alarmTime = now + alarmInterval; - } - - long token = Binder.clearCallingIdentity(); - try { - AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent pi = PendingIntent.getBroadcastAsUser(context, REQUEST_EXPIRE_PASSWORD, - new Intent(ACTION_EXPIRED_PASSWORD_NOTIFICATION), - PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT, - new UserHandle(policy.mUserHandle)); - am.cancel(pi); - if (alarmTime != 0) { - am.set(AlarmManager.RTC, alarmTime, pi); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private IPowerManager getIPowerManager() { - if (mIPowerManager == null) { - IBinder b = ServiceManager.getService(Context.POWER_SERVICE); - mIPowerManager = IPowerManager.Stub.asInterface(b); - } - return mIPowerManager; - } - - private IWindowManager getWindowManager() { - if (mIWindowManager == null) { - IBinder b = ServiceManager.getService(Context.WINDOW_SERVICE); - mIWindowManager = IWindowManager.Stub.asInterface(b); - } - return mIWindowManager; - } - - private NotificationManager getNotificationManager() { - if (mNotificationManager == null) { - mNotificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - } - return mNotificationManager; - } - - ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle) { - ActiveAdmin admin = getUserData(userHandle).mAdminMap.get(who); - if (admin != null - && who.getPackageName().equals(admin.info.getActivityInfo().packageName) - && who.getClassName().equals(admin.info.getActivityInfo().name)) { - return admin; - } - return null; - } - - ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy) - throws SecurityException { - final int callingUid = Binder.getCallingUid(); - final int userHandle = UserHandle.getUserId(callingUid); - final DevicePolicyData policy = getUserData(userHandle); - if (who != null) { - ActiveAdmin admin = policy.mAdminMap.get(who); - if (admin == null) { - throw new SecurityException("No active admin " + who); - } - if (admin.getUid() != callingUid) { - throw new SecurityException("Admin " + who + " is not owned by uid " - + Binder.getCallingUid()); - } - if (!admin.info.usesPolicy(reqPolicy)) { - throw new SecurityException("Admin " + admin.info.getComponent() - + " did not specify uses-policy for: " - + admin.info.getTagForPolicy(reqPolicy)); - } - return admin; - } else { - final int N = policy.mAdminList.size(); - for (int i=0; i 0) { - for (int i = 0; i < count; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - if (admin.info.usesPolicy(reqPolicy)) { - sendAdminCommandLocked(admin, action); - } - } - } - } - - void removeActiveAdminLocked(final ComponentName adminReceiver, int userHandle) { - final ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle); - if (admin != null) { - sendAdminCommandLocked(admin, - DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLED, - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - synchronized (DevicePolicyManagerService.this) { - int userHandle = admin.getUserHandle().getIdentifier(); - DevicePolicyData policy = getUserData(userHandle); - boolean doProxyCleanup = admin.info.usesPolicy( - DeviceAdminInfo.USES_POLICY_SETS_GLOBAL_PROXY); - policy.mAdminList.remove(admin); - policy.mAdminMap.remove(adminReceiver); - validatePasswordOwnerLocked(policy); - syncDeviceCapabilitiesLocked(policy); - if (doProxyCleanup) { - resetGlobalProxyLocked(getUserData(userHandle)); - } - saveSettingsLocked(userHandle); - updateMaximumTimeToLockLocked(policy); - } - } - }); - } - } - - public DeviceAdminInfo findAdmin(ComponentName adminName, int userHandle) { - if (!mHasFeature) { - return null; - } - enforceCrossUserPermission(userHandle); - Intent resolveIntent = new Intent(); - resolveIntent.setComponent(adminName); - List infos = mContext.getPackageManager().queryBroadcastReceivers( - resolveIntent, - PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, - userHandle); - if (infos == null || infos.size() <= 0) { - throw new IllegalArgumentException("Unknown admin: " + adminName); - } - - try { - return new DeviceAdminInfo(mContext, infos.get(0)); - } catch (XmlPullParserException e) { - Slog.w(TAG, "Bad device admin requested for user=" + userHandle + ": " + adminName, e); - return null; - } catch (IOException e) { - Slog.w(TAG, "Bad device admin requested for user=" + userHandle + ": " + adminName, e); - return null; - } - } - - private static JournaledFile makeJournaledFile(int userHandle) { - final String base = userHandle == 0 - ? "/data/system/" + DEVICE_POLICIES_XML - : new File(Environment.getUserSystemDirectory(userHandle), DEVICE_POLICIES_XML) - .getAbsolutePath(); - return new JournaledFile(new File(base), new File(base + ".tmp")); - } - - private void saveSettingsLocked(int userHandle) { - DevicePolicyData policy = getUserData(userHandle); - JournaledFile journal = makeJournaledFile(userHandle); - FileOutputStream stream = null; - try { - stream = new FileOutputStream(journal.chooseForWrite(), false); - XmlSerializer out = new FastXmlSerializer(); - out.setOutput(stream, "utf-8"); - out.startDocument(null, true); - - out.startTag(null, "policies"); - - final int N = policy.mAdminList.size(); - for (int i=0; i= 0) { - out.startTag(null, "password-owner"); - out.attribute(null, "value", Integer.toString(policy.mPasswordOwner)); - out.endTag(null, "password-owner"); - } - - if (policy.mFailedPasswordAttempts != 0) { - out.startTag(null, "failed-password-attempts"); - out.attribute(null, "value", Integer.toString(policy.mFailedPasswordAttempts)); - out.endTag(null, "failed-password-attempts"); - } - - if (policy.mActivePasswordQuality != 0 || policy.mActivePasswordLength != 0 - || policy.mActivePasswordUpperCase != 0 || policy.mActivePasswordLowerCase != 0 - || policy.mActivePasswordLetters != 0 || policy.mActivePasswordNumeric != 0 - || policy.mActivePasswordSymbols != 0 || policy.mActivePasswordNonLetter != 0) { - out.startTag(null, "active-password"); - out.attribute(null, "quality", Integer.toString(policy.mActivePasswordQuality)); - out.attribute(null, "length", Integer.toString(policy.mActivePasswordLength)); - out.attribute(null, "uppercase", Integer.toString(policy.mActivePasswordUpperCase)); - out.attribute(null, "lowercase", Integer.toString(policy.mActivePasswordLowerCase)); - out.attribute(null, "letters", Integer.toString(policy.mActivePasswordLetters)); - out.attribute(null, "numeric", Integer - .toString(policy.mActivePasswordNumeric)); - out.attribute(null, "symbols", Integer.toString(policy.mActivePasswordSymbols)); - out.attribute(null, "nonletter", Integer.toString(policy.mActivePasswordNonLetter)); - out.endTag(null, "active-password"); - } - - out.endTag(null, "policies"); - - out.endDocument(); - stream.close(); - journal.commit(); - sendChangedNotification(userHandle); - } catch (IOException e) { - try { - if (stream != null) { - stream.close(); - } - } catch (IOException ex) { - // Ignore - } - journal.rollback(); - } - } - - private void sendChangedNotification(int userHandle) { - Intent intent = new Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); - intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - long ident = Binder.clearCallingIdentity(); - try { - mContext.sendBroadcastAsUser(intent, new UserHandle(userHandle)); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - private void loadSettingsLocked(DevicePolicyData policy, int userHandle) { - JournaledFile journal = makeJournaledFile(userHandle); - FileInputStream stream = null; - File file = journal.chooseForRead(); - try { - stream = new FileInputStream(file); - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(stream, null); - - int type; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - } - String tag = parser.getName(); - if (!"policies".equals(tag)) { - throw new XmlPullParserException( - "Settings do not start with policies tag: found " + tag); - } - type = parser.next(); - 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; - } - tag = parser.getName(); - if ("admin".equals(tag)) { - String name = parser.getAttributeValue(null, "name"); - try { - DeviceAdminInfo dai = findAdmin( - ComponentName.unflattenFromString(name), userHandle); - if (DBG && (UserHandle.getUserId(dai.getActivityInfo().applicationInfo.uid) - != userHandle)) { - Slog.w(TAG, "findAdmin returned an incorrect uid " - + dai.getActivityInfo().applicationInfo.uid + " for user " - + userHandle); - } - if (dai != null) { - ActiveAdmin ap = new ActiveAdmin(dai); - ap.readFromXml(parser); - policy.mAdminMap.put(ap.info.getComponent(), ap); - policy.mAdminList.add(ap); - } - } catch (RuntimeException e) { - Slog.w(TAG, "Failed loading admin " + name, e); - } - } else if ("failed-password-attempts".equals(tag)) { - policy.mFailedPasswordAttempts = Integer.parseInt( - parser.getAttributeValue(null, "value")); - XmlUtils.skipCurrentTag(parser); - } else if ("password-owner".equals(tag)) { - policy.mPasswordOwner = Integer.parseInt( - parser.getAttributeValue(null, "value")); - XmlUtils.skipCurrentTag(parser); - } else if ("active-password".equals(tag)) { - policy.mActivePasswordQuality = Integer.parseInt( - parser.getAttributeValue(null, "quality")); - policy.mActivePasswordLength = Integer.parseInt( - parser.getAttributeValue(null, "length")); - policy.mActivePasswordUpperCase = Integer.parseInt( - parser.getAttributeValue(null, "uppercase")); - policy.mActivePasswordLowerCase = Integer.parseInt( - parser.getAttributeValue(null, "lowercase")); - policy.mActivePasswordLetters = Integer.parseInt( - parser.getAttributeValue(null, "letters")); - policy.mActivePasswordNumeric = Integer.parseInt( - parser.getAttributeValue(null, "numeric")); - policy.mActivePasswordSymbols = Integer.parseInt( - parser.getAttributeValue(null, "symbols")); - policy.mActivePasswordNonLetter = Integer.parseInt( - parser.getAttributeValue(null, "nonletter")); - XmlUtils.skipCurrentTag(parser); - } else { - Slog.w(TAG, "Unknown tag: " + tag); - XmlUtils.skipCurrentTag(parser); - } - } - } catch (NullPointerException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (NumberFormatException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (XmlPullParserException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (FileNotFoundException e) { - // Don't be noisy, this is normal if we haven't defined any policies. - } catch (IOException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (IndexOutOfBoundsException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } - try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - // Ignore - } - - // Validate that what we stored for the password quality matches - // sufficiently what is currently set. Note that this is only - // a sanity check in case the two get out of sync; this should - // never normally happen. - LockPatternUtils utils = new LockPatternUtils(mContext); - if (utils.getActivePasswordQuality() < policy.mActivePasswordQuality) { - Slog.w(TAG, "Active password quality 0x" - + Integer.toHexString(policy.mActivePasswordQuality) - + " does not match actual quality 0x" - + Integer.toHexString(utils.getActivePasswordQuality())); - policy.mActivePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - policy.mActivePasswordLength = 0; - policy.mActivePasswordUpperCase = 0; - policy.mActivePasswordLowerCase = 0; - policy.mActivePasswordLetters = 0; - policy.mActivePasswordNumeric = 0; - policy.mActivePasswordSymbols = 0; - policy.mActivePasswordNonLetter = 0; - } - - validatePasswordOwnerLocked(policy); - syncDeviceCapabilitiesLocked(policy); - updateMaximumTimeToLockLocked(policy); - } - - static void validateQualityConstant(int quality) { - switch (quality) { - case DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED: - case DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK: - case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: - case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: - case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: - case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: - case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: - return; - } - throw new IllegalArgumentException("Invalid quality constant: 0x" - + Integer.toHexString(quality)); - } - - void validatePasswordOwnerLocked(DevicePolicyData policy) { - if (policy.mPasswordOwner >= 0) { - boolean haveOwner = false; - for (int i = policy.mAdminList.size() - 1; i >= 0; i--) { - if (policy.mAdminList.get(i).getUid() == policy.mPasswordOwner) { - haveOwner = true; - break; - } - } - if (!haveOwner) { - Slog.w(TAG, "Previous password owner " + policy.mPasswordOwner - + " no longer active; disabling"); - policy.mPasswordOwner = -1; - } - } - } - - /** - * Pushes down policy information to the system for any policies related to general device - * capabilities that need to be enforced by lower level services (e.g. Camera services). - */ - void syncDeviceCapabilitiesLocked(DevicePolicyData policy) { - // Ensure the status of the camera is synced down to the system. Interested native services - // should monitor this value and act accordingly. - boolean systemState = SystemProperties.getBoolean(SYSTEM_PROP_DISABLE_CAMERA, false); - boolean cameraDisabled = getCameraDisabled(null, policy.mUserHandle); - if (cameraDisabled != systemState) { - long token = Binder.clearCallingIdentity(); - try { - String value = cameraDisabled ? "1" : "0"; - if (DBG) Slog.v(TAG, "Change in camera state [" - + SYSTEM_PROP_DISABLE_CAMERA + "] = " + value); - SystemProperties.set(SYSTEM_PROP_DISABLE_CAMERA, value); - } finally { - Binder.restoreCallingIdentity(token); - } - } - } - - public void systemReady() { - if (!mHasFeature) { - return; - } - synchronized (this) { - loadSettingsLocked(getUserData(UserHandle.USER_OWNER), UserHandle.USER_OWNER); - loadDeviceOwner(); - } - } - - private void handlePasswordExpirationNotification(DevicePolicyData policy) { - synchronized (this) { - final long now = System.currentTimeMillis(); - final int N = policy.mAdminList.size(); - if (N <= 0) { - return; - } - for (int i=0; i < N; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - if (admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) - && admin.passwordExpirationTimeout > 0L - && admin.passwordExpirationDate > 0L - && now >= admin.passwordExpirationDate - EXPIRATION_GRACE_PERIOD_MS) { - sendAdminCommandLocked(admin, DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING); - } - } - setExpirationAlarmCheckLocked(mContext, policy); - } - } - - private void manageMonitoringCertificateNotification(Intent intent) { - final NotificationManager notificationManager = getNotificationManager(); - - final boolean hasCert = DevicePolicyManager.hasAnyCaCertsInstalled(); - if (! hasCert) { - if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { - UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - for (UserInfo user : um.getUsers()) { - notificationManager.cancelAsUser( - null, MONITORING_CERT_NOTIFICATION_ID, user.getUserHandle()); - } - } - return; - } - final boolean isManaged = getDeviceOwner() != null; - int smallIconId; - String contentText; - if (isManaged) { - contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed, - getDeviceOwnerName()); - smallIconId = R.drawable.stat_sys_certificate_info; - } else { - contentText = mContext.getString(R.string.ssl_ca_cert_noti_by_unknown); - smallIconId = android.R.drawable.stat_sys_warning; - } - - Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO); - dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - dialogIntent.setPackage("com.android.settings"); - // Notification will be sent individually to all users. The activity should start as - // whichever user is current when it starts. - PendingIntent notifyIntent = PendingIntent.getActivityAsUser(mContext, 0, dialogIntent, - PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT); - - Notification noti = new Notification.Builder(mContext) - .setSmallIcon(smallIconId) - .setContentTitle(mContext.getString(R.string.ssl_ca_cert_warning)) - .setContentText(contentText) - .setContentIntent(notifyIntent) - .setPriority(Notification.PRIORITY_HIGH) - .setShowWhen(false) - .build(); - - // If this is a boot intent, this will fire for each user. But if this is a storage changed - // intent, it will fire once, so we need to notify all users. - if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { - UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - for (UserInfo user : um.getUsers()) { - notificationManager.notifyAsUser( - null, MONITORING_CERT_NOTIFICATION_ID, noti, user.getUserHandle()); - } - } else { - notificationManager.notifyAsUser( - null, MONITORING_CERT_NOTIFICATION_ID, noti, UserHandle.CURRENT); - } - } - - /** - * @param adminReceiver The admin to add - * @param refreshing true = update an active admin, no error - */ - public void setActiveAdmin(ComponentName adminReceiver, boolean refreshing, int userHandle) { - if (!mHasFeature) { - return; - } - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.MANAGE_DEVICE_ADMINS, null); - enforceCrossUserPermission(userHandle); - - DevicePolicyData policy = getUserData(userHandle); - DeviceAdminInfo info = findAdmin(adminReceiver, userHandle); - if (info == null) { - throw new IllegalArgumentException("Bad admin: " + adminReceiver); - } - synchronized (this) { - long ident = Binder.clearCallingIdentity(); - try { - if (!refreshing && getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null) { - throw new IllegalArgumentException("Admin is already added"); - } - ActiveAdmin newAdmin = new ActiveAdmin(info); - policy.mAdminMap.put(adminReceiver, newAdmin); - int replaceIndex = -1; - if (refreshing) { - final int N = policy.mAdminList.size(); - for (int i=0; i < N; i++) { - ActiveAdmin oldAdmin = policy.mAdminList.get(i); - if (oldAdmin.info.getComponent().equals(adminReceiver)) { - replaceIndex = i; - break; - } - } - } - if (replaceIndex == -1) { - policy.mAdminList.add(newAdmin); - enableIfNecessary(info.getPackageName(), userHandle); - } else { - policy.mAdminList.set(replaceIndex, newAdmin); - } - saveSettingsLocked(userHandle); - sendAdminCommandLocked(newAdmin, DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - - public boolean isAdminActive(ComponentName adminReceiver, int userHandle) { - if (!mHasFeature) { - return false; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - return getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null; - } - } - - public boolean hasGrantedPolicy(ComponentName adminReceiver, int policyId, int userHandle) { - if (!mHasFeature) { - return false; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - ActiveAdmin administrator = getActiveAdminUncheckedLocked(adminReceiver, userHandle); - if (administrator == null) { - throw new SecurityException("No active admin " + adminReceiver); - } - return administrator.info.usesPolicy(policyId); - } - } - - @SuppressWarnings("unchecked") - public List getActiveAdmins(int userHandle) { - if (!mHasFeature) { - return Collections.EMPTY_LIST; - } - - enforceCrossUserPermission(userHandle); - synchronized (this) { - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - if (N <= 0) { - return null; - } - ArrayList res = new ArrayList(N); - for (int i=0; i= 0 ms"); - } - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD); - // Calling this API automatically bumps the expiration date - final long expiration = timeout > 0L ? (timeout + System.currentTimeMillis()) : 0L; - ap.passwordExpirationDate = expiration; - ap.passwordExpirationTimeout = timeout; - if (timeout > 0L) { - Slog.w(TAG, "setPasswordExpiration(): password will expire on " - + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT) - .format(new Date(expiration))); - } - saveSettingsLocked(userHandle); - // in case this is the first one - setExpirationAlarmCheckLocked(mContext, getUserData(userHandle)); - } - } - - /** - * Return a single admin's expiration cycle time, or the min of all cycle times. - * Returns 0 if not configured. - */ - public long getPasswordExpirationTimeout(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0L; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return admin != null ? admin.passwordExpirationTimeout : 0L; - } - - long timeout = 0L; - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - if (timeout == 0L || (admin.passwordExpirationTimeout != 0L - && timeout > admin.passwordExpirationTimeout)) { - timeout = admin.passwordExpirationTimeout; - } - } - return timeout; - } - } - - /** - * Return a single admin's expiration date/time, or the min (soonest) for all admins. - * Returns 0 if not configured. - */ - private long getPasswordExpirationLocked(ComponentName who, int userHandle) { - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return admin != null ? admin.passwordExpirationDate : 0L; - } - - long timeout = 0L; - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - if (timeout == 0L || (admin.passwordExpirationDate != 0 - && timeout > admin.passwordExpirationDate)) { - timeout = admin.passwordExpirationDate; - } - } - return timeout; - } - - public long getPasswordExpiration(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0L; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - return getPasswordExpirationLocked(who, userHandle); - } - } - - public void setPasswordMinimumUpperCase(ComponentName who, int length, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD); - if (ap.minimumPasswordUpperCase != length) { - ap.minimumPasswordUpperCase = length; - saveSettingsLocked(userHandle); - } - } - } - - public int getPasswordMinimumUpperCase(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - int length = 0; - - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return admin != null ? admin.minimumPasswordUpperCase : length; - } - - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - for (int i=0; i= getPasswordMinimumUpperCase(null, userHandle) - && policy.mActivePasswordLowerCase >= getPasswordMinimumLowerCase(null, userHandle) - && policy.mActivePasswordLetters >= getPasswordMinimumLetters(null, userHandle) - && policy.mActivePasswordNumeric >= getPasswordMinimumNumeric(null, userHandle) - && policy.mActivePasswordSymbols >= getPasswordMinimumSymbols(null, userHandle) - && policy.mActivePasswordNonLetter >= getPasswordMinimumNonLetter(null, userHandle); - } - } - - public int getCurrentFailedPasswordAttempts(int userHandle) { - enforceCrossUserPermission(userHandle); - synchronized (this) { - // This API can only be called by an active device admin, - // so try to retrieve it to check that the caller is one. - getActiveAdminForCallerLocked(null, - DeviceAdminInfo.USES_POLICY_WATCH_LOGIN); - return getUserData(userHandle).mFailedPasswordAttempts; - } - } - - public void setMaximumFailedPasswordsForWipe(ComponentName who, int num, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - // This API can only be called by an active device admin, - // so try to retrieve it to check that the caller is one. - getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_WIPE_DATA); - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_WATCH_LOGIN); - if (ap.maximumFailedPasswordsForWipe != num) { - ap.maximumFailedPasswordsForWipe = num; - saveSettingsLocked(userHandle); - } - } - } - - public int getMaximumFailedPasswordsForWipe(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - DevicePolicyData policy = getUserData(userHandle); - int count = 0; - - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return admin != null ? admin.maximumFailedPasswordsForWipe : count; - } - - final int N = policy.mAdminList.size(); - for (int i=0; i admin.maximumFailedPasswordsForWipe) { - count = admin.maximumFailedPasswordsForWipe; - } - } - return count; - } - } - - public boolean resetPassword(String password, int flags, int userHandle) { - if (!mHasFeature) { - return false; - } - enforceCrossUserPermission(userHandle); - int quality; - synchronized (this) { - // This API can only be called by an active device admin, - // so try to retrieve it to check that the caller is one. - getActiveAdminForCallerLocked(null, - DeviceAdminInfo.USES_POLICY_RESET_PASSWORD); - quality = getPasswordQuality(null, userHandle); - if (quality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { - int realQuality = LockPatternUtils.computePasswordQuality(password); - if (realQuality < quality - && quality != DevicePolicyManager.PASSWORD_QUALITY_COMPLEX) { - Slog.w(TAG, "resetPassword: password quality 0x" - + Integer.toHexString(realQuality) - + " does not meet required quality 0x" - + Integer.toHexString(quality)); - return false; - } - quality = Math.max(realQuality, quality); - } - int length = getPasswordMinimumLength(null, userHandle); - if (password.length() < length) { - Slog.w(TAG, "resetPassword: password length " + password.length() - + " does not meet required length " + length); - return false; - } - if (quality == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX) { - int letters = 0; - int uppercase = 0; - int lowercase = 0; - int numbers = 0; - int symbols = 0; - int nonletter = 0; - for (int i = 0; i < password.length(); i++) { - char c = password.charAt(i); - if (c >= 'A' && c <= 'Z') { - letters++; - uppercase++; - } else if (c >= 'a' && c <= 'z') { - letters++; - lowercase++; - } else if (c >= '0' && c <= '9') { - numbers++; - nonletter++; - } else { - symbols++; - nonletter++; - } - } - int neededLetters = getPasswordMinimumLetters(null, userHandle); - if(letters < neededLetters) { - Slog.w(TAG, "resetPassword: number of letters " + letters - + " does not meet required number of letters " + neededLetters); - return false; - } - int neededNumbers = getPasswordMinimumNumeric(null, userHandle); - if (numbers < neededNumbers) { - Slog.w(TAG, "resetPassword: number of numerical digits " + numbers - + " does not meet required number of numerical digits " - + neededNumbers); - return false; - } - int neededLowerCase = getPasswordMinimumLowerCase(null, userHandle); - if (lowercase < neededLowerCase) { - Slog.w(TAG, "resetPassword: number of lowercase letters " + lowercase - + " does not meet required number of lowercase letters " - + neededLowerCase); - return false; - } - int neededUpperCase = getPasswordMinimumUpperCase(null, userHandle); - if (uppercase < neededUpperCase) { - Slog.w(TAG, "resetPassword: number of uppercase letters " + uppercase - + " does not meet required number of uppercase letters " - + neededUpperCase); - return false; - } - int neededSymbols = getPasswordMinimumSymbols(null, userHandle); - if (symbols < neededSymbols) { - Slog.w(TAG, "resetPassword: number of special symbols " + symbols - + " does not meet required number of special symbols " + neededSymbols); - return false; - } - int neededNonLetter = getPasswordMinimumNonLetter(null, userHandle); - if (nonletter < neededNonLetter) { - Slog.w(TAG, "resetPassword: number of non-letter characters " + nonletter - + " does not meet required number of non-letter characters " - + neededNonLetter); - return false; - } - } - } - - int callingUid = Binder.getCallingUid(); - DevicePolicyData policy = getUserData(userHandle); - if (policy.mPasswordOwner >= 0 && policy.mPasswordOwner != callingUid) { - Slog.w(TAG, "resetPassword: already set by another uid and not entered by user"); - return false; - } - - // Don't do this with the lock held, because it is going to call - // back in to the service. - long ident = Binder.clearCallingIdentity(); - try { - LockPatternUtils utils = new LockPatternUtils(mContext); - utils.saveLockPassword(password, quality, false, userHandle); - synchronized (this) { - int newOwner = (flags&DevicePolicyManager.RESET_PASSWORD_REQUIRE_ENTRY) - != 0 ? callingUid : -1; - if (policy.mPasswordOwner != newOwner) { - policy.mPasswordOwner = newOwner; - saveSettingsLocked(userHandle); - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - - return true; - } - - public void setMaximumTimeToLock(ComponentName who, long timeMs, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_FORCE_LOCK); - if (ap.maximumTimeToUnlock != timeMs) { - ap.maximumTimeToUnlock = timeMs; - saveSettingsLocked(userHandle); - updateMaximumTimeToLockLocked(getUserData(userHandle)); - } - } - } - - void updateMaximumTimeToLockLocked(DevicePolicyData policy) { - long timeMs = getMaximumTimeToLock(null, policy.mUserHandle); - if (policy.mLastMaximumTimeToLock == timeMs) { - return; - } - - long ident = Binder.clearCallingIdentity(); - try { - if (timeMs <= 0) { - timeMs = Integer.MAX_VALUE; - } else { - // Make sure KEEP_SCREEN_ON is disabled, since that - // would allow bypassing of the maximum time to lock. - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0); - } - - policy.mLastMaximumTimeToLock = timeMs; - - try { - getIPowerManager().setMaximumScreenOffTimeoutFromDeviceAdmin((int)timeMs); - } catch (RemoteException e) { - Slog.w(TAG, "Failure talking with power manager", e); - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - public long getMaximumTimeToLock(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - long time = 0; - - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return admin != null ? admin.maximumTimeToUnlock : time; - } - - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - for (int i=0; i admin.maximumTimeToUnlock) { - time = admin.maximumTimeToUnlock; - } - } - return time; - } - } - - public void lockNow() { - if (!mHasFeature) { - return; - } - synchronized (this) { - // This API can only be called by an active device admin, - // so try to retrieve it to check that the caller is one. - getActiveAdminForCallerLocked(null, - DeviceAdminInfo.USES_POLICY_FORCE_LOCK); - lockNowUnchecked(); - } - } - - private void lockNowUnchecked() { - long ident = Binder.clearCallingIdentity(); - try { - // Power off the display - getIPowerManager().goToSleep(SystemClock.uptimeMillis(), - PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN); - // Ensure the device is locked - getWindowManager().lockNow(null); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - private boolean isExtStorageEncrypted() { - String state = SystemProperties.get("vold.decrypt"); - return !"".equals(state); - } - - public boolean installCaCert(byte[] certBuffer) throws RemoteException { - mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null); - KeyChainConnection keyChainConnection = null; - byte[] pemCert; - try { - X509Certificate cert = parseCert(certBuffer); - pemCert = Credentials.convertToPem(cert); - } catch (CertificateException ce) { - Log.e(TAG, "Problem converting cert", ce); - return false; - } catch (IOException ioe) { - Log.e(TAG, "Problem reading cert", ioe); - return false; - } - try { - keyChainConnection = KeyChain.bind(mContext); - try { - keyChainConnection.getService().installCaCertificate(pemCert); - return true; - } finally { - if (keyChainConnection != null) { - keyChainConnection.close(); - keyChainConnection = null; - } - } - } catch (InterruptedException e1) { - Log.w(TAG, "installCaCertsToKeyChain(): ", e1); - Thread.currentThread().interrupt(); - } - return false; - } - - private static X509Certificate parseCert(byte[] certBuffer) - throws CertificateException, IOException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream( - certBuffer)); - } - - public void uninstallCaCert(final byte[] certBuffer) { - mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null); - TrustedCertificateStore certStore = new TrustedCertificateStore(); - String alias = null; - try { - X509Certificate cert = parseCert(certBuffer); - alias = certStore.getCertificateAlias(cert); - } catch (CertificateException ce) { - Log.e(TAG, "Problem creating X509Certificate", ce); - return; - } catch (IOException ioe) { - Log.e(TAG, "Problem reading certificate", ioe); - return; - } - try { - KeyChainConnection keyChainConnection = KeyChain.bind(mContext); - IKeyChainService service = keyChainConnection.getService(); - try { - service.deleteCaCertificate(alias); - } catch (RemoteException e) { - Log.e(TAG, "from CaCertUninstaller: ", e); - } finally { - keyChainConnection.close(); - keyChainConnection = null; - } - } catch (InterruptedException ie) { - Log.w(TAG, "CaCertUninstaller: ", ie); - Thread.currentThread().interrupt(); - } - } - - void wipeDataLocked(int flags) { - // If the SD card is encrypted and non-removable, we have to force a wipe. - boolean forceExtWipe = !Environment.isExternalStorageRemovable() && isExtStorageEncrypted(); - boolean wipeExtRequested = (flags&DevicePolicyManager.WIPE_EXTERNAL_STORAGE) != 0; - - // Note: we can only do the wipe via ExternalStorageFormatter if the volume is not emulated. - if ((forceExtWipe || wipeExtRequested) && !Environment.isExternalStorageEmulated()) { - Intent intent = new Intent(ExternalStorageFormatter.FORMAT_AND_FACTORY_RESET); - intent.putExtra(ExternalStorageFormatter.EXTRA_ALWAYS_RESET, true); - intent.setComponent(ExternalStorageFormatter.COMPONENT_NAME); - mWakeLock.acquire(10000); - mContext.startService(intent); - } else { - try { - RecoverySystem.rebootWipeUserData(mContext); - } catch (IOException e) { - Slog.w(TAG, "Failed requesting data wipe", e); - } - } - } - - public void wipeData(int flags, final int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - // This API can only be called by an active device admin, - // so try to retrieve it to check that the caller is one. - getActiveAdminForCallerLocked(null, - DeviceAdminInfo.USES_POLICY_WIPE_DATA); - long ident = Binder.clearCallingIdentity(); - try { - wipeDeviceOrUserLocked(flags, userHandle); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - - private void wipeDeviceOrUserLocked(int flags, final int userHandle) { - if (userHandle == UserHandle.USER_OWNER) { - wipeDataLocked(flags); - } else { - lockNowUnchecked(); - mHandler.post(new Runnable() { - public void run() { - try { - ActivityManagerNative.getDefault().switchUser(UserHandle.USER_OWNER); - ((UserManager) mContext.getSystemService(Context.USER_SERVICE)) - .removeUser(userHandle); - } catch (RemoteException re) { - // Shouldn't happen - } - } - }); - } - } - - public void getRemoveWarning(ComponentName comp, final RemoteCallback result, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.BIND_DEVICE_ADMIN, null); - - synchronized (this) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(comp, userHandle); - if (admin == null) { - try { - result.sendResult(null); - } catch (RemoteException e) { - } - return; - } - Intent intent = new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLE_REQUESTED); - intent.setComponent(admin.info.getComponent()); - mContext.sendOrderedBroadcastAsUser(intent, new UserHandle(userHandle), - null, new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - try { - result.sendResult(getResultExtras(false)); - } catch (RemoteException e) { - } - } - }, null, Activity.RESULT_OK, null, null); - } - } - - public void setActivePasswordState(int quality, int length, int letters, int uppercase, - int lowercase, int numbers, int symbols, int nonletter, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.BIND_DEVICE_ADMIN, null); - DevicePolicyData p = getUserData(userHandle); - - validateQualityConstant(quality); - - synchronized (this) { - if (p.mActivePasswordQuality != quality || p.mActivePasswordLength != length - || p.mFailedPasswordAttempts != 0 || p.mActivePasswordLetters != letters - || p.mActivePasswordUpperCase != uppercase - || p.mActivePasswordLowerCase != lowercase || p.mActivePasswordNumeric != numbers - || p.mActivePasswordSymbols != symbols || p.mActivePasswordNonLetter != nonletter) { - long ident = Binder.clearCallingIdentity(); - try { - p.mActivePasswordQuality = quality; - p.mActivePasswordLength = length; - p.mActivePasswordLetters = letters; - p.mActivePasswordLowerCase = lowercase; - p.mActivePasswordUpperCase = uppercase; - p.mActivePasswordNumeric = numbers; - p.mActivePasswordSymbols = symbols; - p.mActivePasswordNonLetter = nonletter; - p.mFailedPasswordAttempts = 0; - saveSettingsLocked(userHandle); - updatePasswordExpirationsLocked(userHandle); - setExpirationAlarmCheckLocked(mContext, p); - sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED, - DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, userHandle); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - } - - /** - * Called any time the device password is updated. Resets all password expiration clocks. - */ - private void updatePasswordExpirationsLocked(int userHandle) { - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - if (N > 0) { - for (int i=0; i 0L ? (timeout + System.currentTimeMillis()) : 0L; - admin.passwordExpirationDate = expiration; - } - } - saveSettingsLocked(userHandle); - } - } - - public void reportFailedPasswordAttempt(int userHandle) { - enforceCrossUserPermission(userHandle); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.BIND_DEVICE_ADMIN, null); - - synchronized (this) { - DevicePolicyData policy = getUserData(userHandle); - long ident = Binder.clearCallingIdentity(); - try { - policy.mFailedPasswordAttempts++; - saveSettingsLocked(userHandle); - if (mHasFeature) { - int max = getMaximumFailedPasswordsForWipe(null, userHandle); - if (max > 0 && policy.mFailedPasswordAttempts >= max) { - wipeDeviceOrUserLocked(0, userHandle); - } - sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_FAILED, - DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle); - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - - public void reportSuccessfulPasswordAttempt(int userHandle) { - enforceCrossUserPermission(userHandle); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.BIND_DEVICE_ADMIN, null); - - synchronized (this) { - DevicePolicyData policy = getUserData(userHandle); - if (policy.mFailedPasswordAttempts != 0 || policy.mPasswordOwner >= 0) { - long ident = Binder.clearCallingIdentity(); - try { - policy.mFailedPasswordAttempts = 0; - policy.mPasswordOwner = -1; - saveSettingsLocked(userHandle); - if (mHasFeature) { - sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_SUCCEEDED, - DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle); - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - } - - public ComponentName setGlobalProxy(ComponentName who, String proxySpec, - String exclusionList, int userHandle) { - if (!mHasFeature) { - return null; - } - enforceCrossUserPermission(userHandle); - synchronized(this) { - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - - // Only check if owner has set global proxy. We don't allow other users to set it. - DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); - ActiveAdmin admin = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_SETS_GLOBAL_PROXY); - - // Scan through active admins and find if anyone has already - // set the global proxy. - Set compSet = policy.mAdminMap.keySet(); - for (ComponentName component : compSet) { - ActiveAdmin ap = policy.mAdminMap.get(component); - if ((ap.specifiesGlobalProxy) && (!component.equals(who))) { - // Another admin already sets the global proxy - // Return it to the caller. - return component; - } - } - - // If the user is not the owner, don't set the global proxy. Fail silently. - if (UserHandle.getCallingUserId() != UserHandle.USER_OWNER) { - Slog.w(TAG, "Only the owner is allowed to set the global proxy. User " - + userHandle + " is not permitted."); - return null; - } - if (proxySpec == null) { - admin.specifiesGlobalProxy = false; - admin.globalProxySpec = null; - admin.globalProxyExclusionList = null; - } else { - - admin.specifiesGlobalProxy = true; - admin.globalProxySpec = proxySpec; - admin.globalProxyExclusionList = exclusionList; - } - - // Reset the global proxy accordingly - // Do this using system permissions, as apps cannot write to secure settings - long origId = Binder.clearCallingIdentity(); - resetGlobalProxyLocked(policy); - Binder.restoreCallingIdentity(origId); - return null; - } - } - - public ComponentName getGlobalProxyAdmin(int userHandle) { - if (!mHasFeature) { - return null; - } - enforceCrossUserPermission(userHandle); - synchronized(this) { - DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); - // Scan through active admins and find if anyone has already - // set the global proxy. - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - ActiveAdmin ap = policy.mAdminList.get(i); - if (ap.specifiesGlobalProxy) { - // Device admin sets the global proxy - // Return it to the caller. - return ap.info.getComponent(); - } - } - } - // No device admin sets the global proxy. - return null; - } - - private void resetGlobalProxyLocked(DevicePolicyData policy) { - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - ActiveAdmin ap = policy.mAdminList.get(i); - if (ap.specifiesGlobalProxy) { - saveGlobalProxyLocked(ap.globalProxySpec, ap.globalProxyExclusionList); - return; - } - } - // No device admins defining global proxies - reset global proxy settings to none - saveGlobalProxyLocked(null, null); - } - - private void saveGlobalProxyLocked(String proxySpec, String exclusionList) { - if (exclusionList == null) { - exclusionList = ""; - } - if (proxySpec == null) { - proxySpec = ""; - } - // Remove white spaces - proxySpec = proxySpec.trim(); - String data[] = proxySpec.split(":"); - int proxyPort = 8080; - if (data.length > 1) { - try { - proxyPort = Integer.parseInt(data[1]); - } catch (NumberFormatException e) {} - } - exclusionList = exclusionList.trim(); - ContentResolver res = mContext.getContentResolver(); - - ProxyProperties proxyProperties = new ProxyProperties(data[0], proxyPort, exclusionList); - if (!proxyProperties.isValid()) { - Slog.e(TAG, "Invalid proxy properties, ignoring: " + proxyProperties.toString()); - return; - } - Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST, data[0]); - Settings.Global.putInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, proxyPort); - Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST, - exclusionList); - } - - /** - * Set the storage encryption request for a single admin. Returns the new total request - * status (for all admins). - */ - public int setStorageEncryption(ComponentName who, boolean encrypt, int userHandle) { - if (!mHasFeature) { - return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - // Check for permissions - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - // Only owner can set storage encryption - if (userHandle != UserHandle.USER_OWNER - || UserHandle.getCallingUserId() != UserHandle.USER_OWNER) { - Slog.w(TAG, "Only owner is allowed to set storage encryption. User " - + UserHandle.getCallingUserId() + " is not permitted."); - return 0; - } - - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_ENCRYPTED_STORAGE); - - // Quick exit: If the filesystem does not support encryption, we can exit early. - if (!isEncryptionSupported()) { - return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - } - - // (1) Record the value for the admin so it's sticky - if (ap.encryptionRequested != encrypt) { - ap.encryptionRequested = encrypt; - saveSettingsLocked(userHandle); - } - - DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); - // (2) Compute "max" for all admins - boolean newRequested = false; - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - newRequested |= policy.mAdminList.get(i).encryptionRequested; - } - - // Notify OS of new request - setEncryptionRequested(newRequested); - - // Return the new global request status - return newRequested - ? DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE - : DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE; - } - } - - /** - * Get the current storage encryption request status for a given admin, or aggregate of all - * active admins. - */ - public boolean getStorageEncryption(ComponentName who, int userHandle) { - if (!mHasFeature) { - return false; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - // Check for permissions if a particular caller is specified - if (who != null) { - // When checking for a single caller, status is based on caller's request - ActiveAdmin ap = getActiveAdminUncheckedLocked(who, userHandle); - return ap != null ? ap.encryptionRequested : false; - } - - // If no particular caller is specified, return the aggregate set of requests. - // This is short circuited by returning true on the first hit. - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - if (policy.mAdminList.get(i).encryptionRequested) { - return true; - } - } - return false; - } - } - - /** - * Get the current encryption status of the device. - */ - public int getStorageEncryptionStatus(int userHandle) { - if (!mHasFeature) { - // Ok to return current status. - } - enforceCrossUserPermission(userHandle); - return getEncryptionStatus(); - } - - /** - * Hook to low-levels: This should report if the filesystem supports encrypted storage. - */ - private boolean isEncryptionSupported() { - // Note, this can be implemented as - // return getEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - // But is provided as a separate internal method if there's a faster way to do a - // simple check for supported-or-not. - return getEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - } - - /** - * Hook to low-levels: Reporting the current status of encryption. - * @return A value such as {@link DevicePolicyManager#ENCRYPTION_STATUS_UNSUPPORTED} or - * {@link DevicePolicyManager#ENCRYPTION_STATUS_INACTIVE} or - * {@link DevicePolicyManager#ENCRYPTION_STATUS_ACTIVE}. - */ - private int getEncryptionStatus() { - String status = SystemProperties.get("ro.crypto.state", "unsupported"); - if ("encrypted".equalsIgnoreCase(status)) { - return DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE; - } else if ("unencrypted".equalsIgnoreCase(status)) { - return DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE; - } else { - return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - } - } - - /** - * Hook to low-levels: If needed, record the new admin setting for encryption. - */ - private void setEncryptionRequested(boolean encrypt) { - } - - /** - * The system property used to share the state of the camera. The native camera service - * is expected to read this property and act accordingly. - */ - public static final String SYSTEM_PROP_DISABLE_CAMERA = "sys.secpolicy.camera.disabled"; - - /** - * Disables all device cameras according to the specified admin. - */ - public void setCameraDisabled(ComponentName who, boolean disabled, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA); - if (ap.disableCamera != disabled) { - ap.disableCamera = disabled; - saveSettingsLocked(userHandle); - } - syncDeviceCapabilitiesLocked(getUserData(userHandle)); - } - } - - /** - * Gets whether or not all device cameras are disabled for a given admin, or disabled for any - * active admins. - */ - public boolean getCameraDisabled(ComponentName who, int userHandle) { - if (!mHasFeature) { - return false; - } - synchronized (this) { - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return (admin != null) ? admin.disableCamera : false; - } - - DevicePolicyData policy = getUserData(userHandle); - // Determine whether or not the device camera is disabled for any active admins. - final int N = policy.mAdminList.size(); - for (int i = 0; i < N; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - if (admin.disableCamera) { - return true; - } - } - return false; - } - } - - /** - * Selectively disable keyguard features. - */ - public void setKeyguardDisabledFeatures(ComponentName who, int which, int userHandle) { - if (!mHasFeature) { - return; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who == null) { - throw new NullPointerException("ComponentName is null"); - } - ActiveAdmin ap = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES); - if (ap.disabledKeyguardFeatures != which) { - ap.disabledKeyguardFeatures = which; - saveSettingsLocked(userHandle); - } - syncDeviceCapabilitiesLocked(getUserData(userHandle)); - } - } - - /** - * Gets the disabled state for features in keyguard for the given admin, - * or the aggregate of all active admins if who is null. - */ - public int getKeyguardDisabledFeatures(ComponentName who, int userHandle) { - if (!mHasFeature) { - return 0; - } - enforceCrossUserPermission(userHandle); - synchronized (this) { - if (who != null) { - ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); - return (admin != null) ? admin.disabledKeyguardFeatures : 0; - } - - // Determine which keyguard features are disabled for any active admins. - DevicePolicyData policy = getUserData(userHandle); - final int N = policy.mAdminList.size(); - int which = 0; - for (int i = 0; i < N; i++) { - ActiveAdmin admin = policy.mAdminList.get(i); - which |= admin.disabledKeyguardFeatures; - } - return which; - } - } - - @Override - public boolean setDeviceOwner(String packageName, String ownerName) { - if (!mHasFeature) { - return false; - } - if (packageName == null - || !DeviceOwner.isInstalled(packageName, mContext.getPackageManager())) { - throw new IllegalArgumentException("Invalid package name " + packageName - + " for device owner"); - } - synchronized (this) { - if (mDeviceOwner == null && !isDeviceProvisioned()) { - mDeviceOwner = new DeviceOwner(packageName, ownerName); - mDeviceOwner.writeOwnerFile(); - return true; - } else { - throw new IllegalStateException("Trying to set device owner to " + packageName - + ", owner=" + mDeviceOwner.getPackageName() - + ", device_provisioned=" + isDeviceProvisioned()); - } - } - } - - @Override - public boolean isDeviceOwner(String packageName) { - if (!mHasFeature) { - return false; - } - synchronized (this) { - return mDeviceOwner != null - && mDeviceOwner.getPackageName().equals(packageName); - } - } - - @Override - public String getDeviceOwner() { - if (!mHasFeature) { - return null; - } - synchronized (this) { - if (mDeviceOwner != null) { - return mDeviceOwner.getPackageName(); - } - } - return null; - } - - @Override - public String getDeviceOwnerName() { - if (!mHasFeature) { - return null; - } - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USERS, null); - synchronized (this) { - if (mDeviceOwner != null) { - return mDeviceOwner.getName(); - } - } - return null; - } - - private boolean isDeviceProvisioned() { - return Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 0) > 0; - } - - private void enforceCrossUserPermission(int userHandle) { - if (userHandle < 0) { - throw new IllegalArgumentException("Invalid userId " + userHandle); - } - final int callingUid = Binder.getCallingUid(); - if (userHandle == UserHandle.getUserId(callingUid)) return; - if (callingUid != Process.SYSTEM_UID && callingUid != 0) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "Must be system or have" - + " INTERACT_ACROSS_USERS_FULL permission"); - } - } - - private void enableIfNecessary(String packageName, int userId) { - try { - IPackageManager ipm = AppGlobals.getPackageManager(); - ApplicationInfo ai = ipm.getApplicationInfo(packageName, - PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, - userId); - if (ai.enabledSetting - == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { - ipm.setApplicationEnabledSetting(packageName, - PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, - PackageManager.DONT_KILL_APP, userId, "DevicePolicyManager"); - } - } catch (RemoteException e) { - } - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) - != PackageManager.PERMISSION_GRANTED) { - - pw.println("Permission Denial: can't dump DevicePolicyManagerService from from pid=" - + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return; - } - - final Printer p = new PrintWriterPrinter(pw); - - synchronized (this) { - p.println("Current Device Policy Manager state:"); - - int userCount = mUserData.size(); - for (int u = 0; u < userCount; u++) { - DevicePolicyData policy = getUserData(mUserData.keyAt(u)); - p.println(" Enabled Device Admins (User " + policy.mUserHandle + "):"); - final int N = policy.mAdminList.size(); - for (int i=0; i mAllPackages; - private PackageManager mPackageManager; - // version & signature info of each app in a restore set - private HashMap mRestoredSignatures; - // The version info of each backed-up app as read from the state file - private HashMap mStateVersions = new HashMap(); - - private final HashSet mExisting = new HashSet(); - private int mStoredSdkVersion; - private String mStoredIncrementalVersion; - private boolean mHasMetadata; - - public class Metadata { - public int versionCode; - public Signature[] signatures; - - Metadata(int version, Signature[] sigs) { - versionCode = version; - signatures = sigs; - } - } - - // We're constructed with the set of applications that are participating - // in backup. This set changes as apps are installed & removed. - PackageManagerBackupAgent(PackageManager packageMgr, List packages) { - mPackageManager = packageMgr; - mAllPackages = packages; - mRestoredSignatures = null; - mHasMetadata = false; - } - - public boolean hasMetadata() { - return mHasMetadata; - } - - public Metadata getRestoredMetadata(String packageName) { - if (mRestoredSignatures == null) { - Slog.w(TAG, "getRestoredMetadata() before metadata read!"); - return null; - } - - return mRestoredSignatures.get(packageName); - } - - public Set getRestoredPackages() { - if (mRestoredSignatures == null) { - Slog.w(TAG, "getRestoredPackages() before metadata read!"); - return null; - } - - // This is technically the set of packages on the originating handset - // that had backup agents at all, not limited to the set of packages - // that had actually contributed a restore dataset, but it's a - // close enough approximation for our purposes and does not require any - // additional involvement by the transport to obtain. - return mRestoredSignatures.keySet(); - } - - // The backed up data is the signature block for each app, keyed by - // the package name. - public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState) { - if (DEBUG) Slog.v(TAG, "onBackup()"); - - ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); // we'll reuse these - DataOutputStream outputBufferStream = new DataOutputStream(outputBuffer); - parseStateFile(oldState); - - // If the stored version string differs, we need to re-backup all - // of the metadata. We force this by removing everything from the - // "already backed up" map built by parseStateFile(). - if (mStoredIncrementalVersion == null - || !mStoredIncrementalVersion.equals(Build.VERSION.INCREMENTAL)) { - Slog.i(TAG, "Previous metadata " + mStoredIncrementalVersion + " mismatch vs " - + Build.VERSION.INCREMENTAL + " - rewriting"); - mExisting.clear(); - } - - try { - /* - * Global metadata: - * - * int SDKversion -- the SDK version of the OS itself on the device - * that produced this backup set. Used to reject - * backups from later OSes onto earlier ones. - * String incremental -- the incremental release name of the OS stored in - * the backup set. - */ - if (!mExisting.contains(GLOBAL_METADATA_KEY)) { - if (DEBUG) Slog.v(TAG, "Storing global metadata key"); - outputBufferStream.writeInt(Build.VERSION.SDK_INT); - outputBufferStream.writeUTF(Build.VERSION.INCREMENTAL); - writeEntity(data, GLOBAL_METADATA_KEY, outputBuffer.toByteArray()); - } else { - if (DEBUG) Slog.v(TAG, "Global metadata key already stored"); - // don't consider it to have been skipped/deleted - mExisting.remove(GLOBAL_METADATA_KEY); - } - - // For each app we have on device, see if we've backed it up yet. If not, - // write its signature block to the output, keyed on the package name. - for (PackageInfo pkg : mAllPackages) { - String packName = pkg.packageName; - if (packName.equals(GLOBAL_METADATA_KEY)) { - // We've already handled the metadata key; skip it here - continue; - } else { - PackageInfo info = null; - try { - info = mPackageManager.getPackageInfo(packName, - PackageManager.GET_SIGNATURES); - } catch (NameNotFoundException e) { - // Weird; we just found it, and now are told it doesn't exist. - // Treat it as having been removed from the device. - mExisting.add(packName); - continue; - } - - if (mExisting.contains(packName)) { - // We have backed up this app before. Check whether the version - // of the backup matches the version of the current app; if they - // don't match, the app has been updated and we need to store its - // metadata again. In either case, take it out of mExisting so that - // we don't consider it deleted later. - mExisting.remove(packName); - if (info.versionCode == mStateVersions.get(packName).versionCode) { - continue; - } - } - - if (info.signatures == null || info.signatures.length == 0) - { - Slog.w(TAG, "Not backing up package " + packName - + " since it appears to have no signatures."); - continue; - } - - // We need to store this app's metadata - /* - * Metadata for each package: - * - * int version -- [4] the package's versionCode - * byte[] signatures -- [len] flattened Signature[] of the package - */ - - // marshal the version code in a canonical form - outputBuffer.reset(); - outputBufferStream.writeInt(info.versionCode); - writeSignatureArray(outputBufferStream, info.signatures); - - if (DEBUG) { - Slog.v(TAG, "+ writing metadata for " + packName - + " version=" + info.versionCode - + " entityLen=" + outputBuffer.size()); - } - - // Now we can write the backup entity for this package - writeEntity(data, packName, outputBuffer.toByteArray()); - } - } - - // At this point, the only entries in 'existing' are apps that were - // mentioned in the saved state file, but appear to no longer be present - // on the device. Write a deletion entity for them. - for (String app : mExisting) { - if (DEBUG) Slog.v(TAG, "- removing metadata for deleted pkg " + app); - try { - data.writeEntityHeader(app, -1); - } catch (IOException e) { - Slog.e(TAG, "Unable to write package deletions!"); - return; - } - } - } catch (IOException e) { - // Real error writing data - Slog.e(TAG, "Unable to write package backup data file!"); - return; - } - - // Finally, write the new state blob -- just the list of all apps we handled - writeStateFile(mAllPackages, newState); - } - - private static void writeEntity(BackupDataOutput data, String key, byte[] bytes) - throws IOException { - data.writeEntityHeader(key, bytes.length); - data.writeEntityData(bytes, bytes.length); - } - - // "Restore" here is a misnomer. What we're really doing is reading back the - // set of app signatures associated with each backed-up app in this restore - // image. We'll use those later to determine what we can legitimately restore. - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) - throws IOException { - List restoredApps = new ArrayList(); - HashMap sigMap = new HashMap(); - if (DEBUG) Slog.v(TAG, "onRestore()"); - int storedSystemVersion = -1; - - while (data.readNextHeader()) { - String key = data.getKey(); - int dataSize = data.getDataSize(); - - if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize); - - // generic setup to parse any entity data - byte[] inputBytes = new byte[dataSize]; - data.readEntityData(inputBytes, 0, dataSize); - ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes); - DataInputStream inputBufferStream = new DataInputStream(inputBuffer); - - if (key.equals(GLOBAL_METADATA_KEY)) { - int storedSdkVersion = inputBufferStream.readInt(); - if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion); - if (storedSystemVersion > Build.VERSION.SDK_INT) { - // returning before setting the sig map means we rejected the restore set - Slog.w(TAG, "Restore set was from a later version of Android; not restoring"); - return; - } - mStoredSdkVersion = storedSdkVersion; - mStoredIncrementalVersion = inputBufferStream.readUTF(); - mHasMetadata = true; - if (DEBUG) { - Slog.i(TAG, "Restore set version " + storedSystemVersion - + " is compatible with OS version " + Build.VERSION.SDK_INT - + " (" + mStoredIncrementalVersion + " vs " - + Build.VERSION.INCREMENTAL + ")"); - } - } else { - // it's a file metadata record - int versionCode = inputBufferStream.readInt(); - Signature[] sigs = readSignatureArray(inputBufferStream); - if (DEBUG) { - Slog.i(TAG, " read metadata for " + key - + " dataSize=" + dataSize - + " versionCode=" + versionCode + " sigs=" + sigs); - } - - if (sigs == null || sigs.length == 0) { - Slog.w(TAG, "Not restoring package " + key - + " since it appears to have no signatures."); - continue; - } - - ApplicationInfo app = new ApplicationInfo(); - app.packageName = key; - restoredApps.add(app); - sigMap.put(key, new Metadata(versionCode, sigs)); - } - } - - // On successful completion, cache the signature map for the Backup Manager to use - mRestoredSignatures = sigMap; - } - - private static void writeSignatureArray(DataOutputStream out, Signature[] sigs) - throws IOException { - // write the number of signatures in the array - out.writeInt(sigs.length); - - // write the signatures themselves, length + flattened buffer - for (Signature sig : sigs) { - byte[] flat = sig.toByteArray(); - out.writeInt(flat.length); - out.write(flat); - } - } - - private static Signature[] readSignatureArray(DataInputStream in) { - try { - int num; - try { - num = in.readInt(); - } catch (EOFException e) { - // clean termination - Slog.w(TAG, "Read empty signature block"); - return null; - } - - if (DEBUG) Slog.v(TAG, " ... unflatten read " + num); - - // Sensical? - if (num > 20) { - Slog.e(TAG, "Suspiciously large sig count in restore data; aborting"); - throw new IllegalStateException("Bad restore state"); - } - - Signature[] sigs = new Signature[num]; - for (int i = 0; i < num; i++) { - int len = in.readInt(); - byte[] flatSig = new byte[len]; - in.read(flatSig); - sigs[i] = new Signature(flatSig); - } - return sigs; - } catch (IOException e) { - Slog.e(TAG, "Unable to read signatures"); - return null; - } - } - - // Util: parse out an existing state file into a usable structure - private void parseStateFile(ParcelFileDescriptor stateFile) { - mExisting.clear(); - mStateVersions.clear(); - mStoredSdkVersion = 0; - mStoredIncrementalVersion = null; - - // The state file is just the list of app names we have stored signatures for - // with the exception of the metadata block, to which is also appended the - // version numbers corresponding with the last time we wrote this PM block. - // If they mismatch the current system, we'll re-store the metadata key. - FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor()); - DataInputStream in = new DataInputStream(instream); - - int bufSize = 256; - byte[] buf = new byte[bufSize]; - try { - String pkg = in.readUTF(); - if (pkg.equals(GLOBAL_METADATA_KEY)) { - mStoredSdkVersion = in.readInt(); - mStoredIncrementalVersion = in.readUTF(); - mExisting.add(GLOBAL_METADATA_KEY); - } else { - Slog.e(TAG, "No global metadata in state file!"); - return; - } - - // The global metadata was first; now read all the apps - while (true) { - pkg = in.readUTF(); - int versionCode = in.readInt(); - mExisting.add(pkg); - mStateVersions.put(pkg, new Metadata(versionCode, null)); - } - } catch (EOFException eof) { - // safe; we're done - } catch (IOException e) { - // whoops, bad state file. abort. - Slog.e(TAG, "Unable to read Package Manager state file: " + e); - } - } - - // Util: write out our new backup state file - private void writeStateFile(List pkgs, ParcelFileDescriptor stateFile) { - FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor()); - DataOutputStream out = new DataOutputStream(outstream); - - try { - // by the time we get here we know we've stored the global metadata record - out.writeUTF(GLOBAL_METADATA_KEY); - out.writeInt(Build.VERSION.SDK_INT); - out.writeUTF(Build.VERSION.INCREMENTAL); - - // now write all the app names too - for (PackageInfo pkg : pkgs) { - out.writeUTF(pkg.packageName); - out.writeInt(pkg.versionCode); - } - } catch (IOException e) { - Slog.e(TAG, "Unable to write package manager state file!"); - return; - } - } -} diff --git a/services/java/com/android/server/PreferredComponent.java b/services/java/com/android/server/PreferredComponent.java deleted file mode 100644 index a7af252ca2ed..000000000000 --- a/services/java/com/android/server/PreferredComponent.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 com.android.server; - -import com.android.internal.util.XmlUtils; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import android.content.ComponentName; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; -import android.content.pm.ResolveInfo; -import android.util.Slog; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.List; - -public class PreferredComponent { - private static final String TAG_SET = "set"; - private static final String ATTR_ALWAYS = "always"; // boolean - private static final String ATTR_MATCH = "match"; // number - private static final String ATTR_NAME = "name"; // component name - private static final String ATTR_SET = "set"; // number - - public final int mMatch; - public final ComponentName mComponent; - // Whether this is to be the one that's always chosen. If false, it's the most recently chosen. - public boolean mAlways; - - private final String[] mSetPackages; - private final String[] mSetClasses; - private final String[] mSetComponents; - private final String mShortComponent; - private String mParseError; - - private final Callbacks mCallbacks; - - public interface Callbacks { - public boolean onReadTag(String tagName, XmlPullParser parser) - throws XmlPullParserException, IOException; - } - - public PreferredComponent(Callbacks callbacks, int match, ComponentName[] set, - ComponentName component, boolean always) { - mCallbacks = callbacks; - mMatch = match&IntentFilter.MATCH_CATEGORY_MASK; - mComponent = component; - mAlways = always; - mShortComponent = component.flattenToShortString(); - mParseError = null; - if (set != null) { - final int N = set.length; - String[] myPackages = new String[N]; - String[] myClasses = new String[N]; - String[] myComponents = new String[N]; - for (int i=0; i 0 ? new String[setCount] : null; - String[] myClasses = setCount > 0 ? new String[setCount] : null; - String[] myComponents = setCount > 0 ? new String[setCount] : null; - - int setPos = 0; - - int outerDepth = parser.getDepth(); - int type; - 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(); - //Log.i(TAG, "Parse outerDepth=" + outerDepth + " depth=" - // + parser.getDepth() + " tag=" + tagName); - if (tagName.equals(TAG_SET)) { - String name = parser.getAttributeValue(null, ATTR_NAME); - if (name == null) { - if (mParseError == null) { - mParseError = "No name in set tag in preferred activity " - + mShortComponent; - } - } else if (setPos >= setCount) { - if (mParseError == null) { - mParseError = "Too many set tags in preferred activity " - + mShortComponent; - } - } else { - ComponentName cn = ComponentName.unflattenFromString(name); - if (cn == null) { - if (mParseError == null) { - mParseError = "Bad set name " + name + " in preferred activity " - + mShortComponent; - } - } else { - myPackages[setPos] = cn.getPackageName(); - myClasses[setPos] = cn.getClassName(); - myComponents[setPos] = name; - setPos++; - } - } - XmlUtils.skipCurrentTag(parser); - } else if (!mCallbacks.onReadTag(tagName, parser)) { - Slog.w("PreferredComponent", "Unknown element: " + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - - if (setPos != setCount) { - if (mParseError == null) { - mParseError = "Not enough set tags (expected " + setCount - + " but found " + setPos + ") in " + mShortComponent; - } - } - - mSetPackages = myPackages; - mSetClasses = myClasses; - mSetComponents = myComponents; - } - - public String getParseError() { - return mParseError; - } - - public void writeToXml(XmlSerializer serializer, boolean full) throws IOException { - final int NS = mSetClasses != null ? mSetClasses.length : 0; - serializer.attribute(null, ATTR_NAME, mShortComponent); - if (full) { - if (mMatch != 0) { - serializer.attribute(null, ATTR_MATCH, Integer.toHexString(mMatch)); - } - serializer.attribute(null, ATTR_ALWAYS, Boolean.toString(mAlways)); - serializer.attribute(null, ATTR_SET, Integer.toString(NS)); - for (int s=0; s query, int priority) { - if (mSetPackages == null) return false; - final int NQ = query.size(); - final int NS = mSetPackages.length; - int numMatch = 0; - for (int i=0; i 0) { - // When the wallpaper has a name, back up the info by itself. - // TODO: Don't rely on the innards of the service object like this! - // TODO: Send a delete for any stored wallpaper image in this case? - files = new String[] { WALLPAPER_INFO }; - keys = new String[] { WALLPAPER_INFO_KEY }; - } - addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, files, keys)); - super.onBackup(oldState, data, newState); - } - - @Override - public void onFullBackup(FullBackupDataOutput data) throws IOException { - // At present we back up only the wallpaper - fullWallpaperBackup(data); - } - - private void fullWallpaperBackup(FullBackupDataOutput output) { - // Back up the data files directly. We do them in this specific order -- - // info file followed by image -- because then we need take no special - // steps during restore; the restore will happen properly when the individual - // files are restored piecemeal. - FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, - WALLPAPER_INFO_DIR, WALLPAPER_INFO, output.getData()); - FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, - WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output.getData()); - } - - @Override - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) - throws IOException { - // On restore, we also support a previous data schema "system_files" - addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, - new String[] { WALLPAPER_IMAGE, WALLPAPER_INFO }, - new String[] { WALLPAPER_IMAGE_KEY, WALLPAPER_INFO_KEY} )); - addHelper("system_files", new WallpaperBackupHelper(SystemBackupAgent.this, - new String[] { WALLPAPER_IMAGE }, - new String[] { WALLPAPER_IMAGE_KEY} )); - - try { - super.onRestore(data, appVersionCode, newState); - - WallpaperManagerService wallpaper = (WallpaperManagerService)ServiceManager.getService( - Context.WALLPAPER_SERVICE); - wallpaper.settingsRestored(); - } catch (IOException ex) { - // If there was a failure, delete everything for the wallpaper, this is too aggressive, - // but this is hopefully a rare failure. - Slog.d(TAG, "restore failed", ex); - (new File(WALLPAPER_IMAGE)).delete(); - (new File(WALLPAPER_INFO)).delete(); - } - } - - @Override - public void onRestoreFile(ParcelFileDescriptor data, long size, - int type, String domain, String path, long mode, long mtime) - throws IOException { - Slog.i(TAG, "Restoring file domain=" + domain + " path=" + path); - - // Bits to indicate postprocessing we may need to perform - boolean restoredWallpaper = false; - - File outFile = null; - // Various domain+files we understand a priori - if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { - if (path.equals(WALLPAPER_INFO_FILENAME)) { - outFile = new File(WALLPAPER_INFO); - restoredWallpaper = true; - } else if (path.equals(WALLPAPER_IMAGE_FILENAME)) { - outFile = new File(WALLPAPER_IMAGE); - restoredWallpaper = true; - } - } - - try { - if (outFile == null) { - Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]"); - } - FullBackup.restoreFile(data, size, type, mode, mtime, outFile); - - if (restoredWallpaper) { - WallpaperManagerService wallpaper = - (WallpaperManagerService)ServiceManager.getService( - Context.WALLPAPER_SERVICE); - wallpaper.settingsRestored(); - } - } catch (IOException e) { - if (restoredWallpaper) { - // Make sure we wind up in a good state - (new File(WALLPAPER_IMAGE)).delete(); - (new File(WALLPAPER_INFO)).delete(); - } - } - } -} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index dd1c9047e21c..3a1c747cef77 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -51,7 +51,11 @@ import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accounts.AccountManagerService; import com.android.server.am.ActivityManagerService; import com.android.server.am.BatteryStatsService; +import com.android.server.appwidget.AppWidgetService; +import com.android.server.backup.BackupManagerService; +import com.android.server.clipboard.ClipboardService; import com.android.server.content.ContentService; +import com.android.server.devicepolicy.DevicePolicyManagerService; import com.android.server.display.DisplayManagerService; import com.android.server.dreams.DreamManagerService; import com.android.server.input.InputManagerService; @@ -67,6 +71,7 @@ import com.android.server.power.ShutdownThread; import com.android.server.print.PrintManagerService; import com.android.server.search.SearchManagerService; import com.android.server.usb.UsbService; +import com.android.server.wallpaper.WallpaperManagerService; import com.android.server.wifi.WifiService; import com.android.server.wm.WindowManagerService; diff --git a/services/java/com/android/server/WallpaperManagerService.java b/services/java/com/android/server/WallpaperManagerService.java deleted file mode 100644 index e6b6b93a1d46..000000000000 --- a/services/java/com/android/server/WallpaperManagerService.java +++ /dev/null @@ -1,1345 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import static android.os.ParcelFileDescriptor.*; - -import android.app.ActivityManagerNative; -import android.app.AppGlobals; -import android.app.IUserSwitchObserver; -import android.app.IWallpaperManager; -import android.app.IWallpaperManagerCallback; -import android.app.PendingIntent; -import android.app.WallpaperInfo; -import android.app.backup.BackupManager; -import android.app.backup.WallpaperBackupHelper; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.UserInfo; -import android.content.res.Resources; -import android.graphics.Point; -import android.os.Binder; -import android.os.Bundle; -import android.os.Environment; -import android.os.FileUtils; -import android.os.IBinder; -import android.os.IRemoteCallback; -import android.os.RemoteException; -import android.os.FileObserver; -import android.os.ParcelFileDescriptor; -import android.os.RemoteCallbackList; -import android.os.SELinux; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.UserManager; -import android.service.wallpaper.IWallpaperConnection; -import android.service.wallpaper.IWallpaperEngine; -import android.service.wallpaper.IWallpaperService; -import android.service.wallpaper.WallpaperService; -import android.util.Slog; -import android.util.SparseArray; -import android.util.Xml; -import android.view.Display; -import android.view.IWindowManager; -import android.view.WindowManager; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.PrintWriter; -import java.util.List; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import com.android.internal.content.PackageMonitor; -import com.android.internal.util.FastXmlSerializer; -import com.android.internal.util.JournaledFile; - -class WallpaperManagerService extends IWallpaperManager.Stub { - static final String TAG = "WallpaperService"; - static final boolean DEBUG = false; - - final Object mLock = new Object[0]; - - /** - * Minimum time between crashes of a wallpaper service for us to consider - * restarting it vs. just reverting to the static wallpaper. - */ - static final long MIN_WALLPAPER_CRASH_TIME = 10000; - static final String WALLPAPER = "wallpaper"; - static final String WALLPAPER_INFO = "wallpaper_info.xml"; - - /** - * Name of the component used to display bitmap wallpapers from either the gallery or - * built-in wallpapers. - */ - static final ComponentName IMAGE_WALLPAPER = new ComponentName("com.android.systemui", - "com.android.systemui.ImageWallpaper"); - - /** - * Observes the wallpaper for changes and notifies all IWallpaperServiceCallbacks - * that the wallpaper has changed. The CREATE is triggered when there is no - * wallpaper set and is created for the first time. The CLOSE_WRITE is triggered - * everytime the wallpaper is changed. - */ - private class WallpaperObserver extends FileObserver { - - final WallpaperData mWallpaper; - final File mWallpaperDir; - final File mWallpaperFile; - - public WallpaperObserver(WallpaperData wallpaper) { - super(getWallpaperDir(wallpaper.userId).getAbsolutePath(), - CLOSE_WRITE | DELETE | DELETE_SELF); - mWallpaperDir = getWallpaperDir(wallpaper.userId); - mWallpaper = wallpaper; - mWallpaperFile = new File(mWallpaperDir, WALLPAPER); - } - - @Override - public void onEvent(int event, String path) { - if (path == null) { - return; - } - synchronized (mLock) { - // changing the wallpaper means we'll need to back up the new one - long origId = Binder.clearCallingIdentity(); - BackupManager bm = new BackupManager(mContext); - bm.dataChanged(); - Binder.restoreCallingIdentity(origId); - - File changedFile = new File(mWallpaperDir, path); - if (mWallpaperFile.equals(changedFile)) { - notifyCallbacksLocked(mWallpaper); - if (mWallpaper.wallpaperComponent == null || event != CLOSE_WRITE - || mWallpaper.imageWallpaperPending) { - if (event == CLOSE_WRITE) { - mWallpaper.imageWallpaperPending = false; - } - bindWallpaperComponentLocked(IMAGE_WALLPAPER, true, - false, mWallpaper, null); - saveSettingsLocked(mWallpaper); - } - } - } - } - } - - final Context mContext; - final IWindowManager mIWindowManager; - final IPackageManager mIPackageManager; - final MyPackageMonitor mMonitor; - WallpaperData mLastWallpaper; - - SparseArray mWallpaperMap = new SparseArray(); - - int mCurrentUserId; - - static class WallpaperData { - - int userId; - - File wallpaperFile; - - /** - * Client is currently writing a new image wallpaper. - */ - boolean imageWallpaperPending; - - /** - * Resource name if using a picture from the wallpaper gallery - */ - String name = ""; - - /** - * The component name of the currently set live wallpaper. - */ - ComponentName wallpaperComponent; - - /** - * The component name of the wallpaper that should be set next. - */ - ComponentName nextWallpaperComponent; - - WallpaperConnection connection; - long lastDiedTime; - boolean wallpaperUpdating; - WallpaperObserver wallpaperObserver; - - /** - * List of callbacks registered they should each be notified when the wallpaper is changed. - */ - private RemoteCallbackList callbacks - = new RemoteCallbackList(); - - int width = -1; - int height = -1; - - WallpaperData(int userId) { - this.userId = userId; - wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER); - } - } - - class WallpaperConnection extends IWallpaperConnection.Stub - implements ServiceConnection { - final WallpaperInfo mInfo; - final Binder mToken = new Binder(); - IWallpaperService mService; - IWallpaperEngine mEngine; - WallpaperData mWallpaper; - IRemoteCallback mReply; - - boolean mDimensionsChanged = false; - - public WallpaperConnection(WallpaperInfo info, WallpaperData wallpaper) { - mInfo = info; - mWallpaper = wallpaper; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - synchronized (mLock) { - if (mWallpaper.connection == this) { - mWallpaper.lastDiedTime = SystemClock.uptimeMillis(); - mService = IWallpaperService.Stub.asInterface(service); - attachServiceLocked(this, mWallpaper); - // XXX should probably do saveSettingsLocked() later - // when we have an engine, but I'm not sure about - // locking there and anyway we always need to be able to - // recover if there is something wrong. - saveSettingsLocked(mWallpaper); - } - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - synchronized (mLock) { - mService = null; - mEngine = null; - if (mWallpaper.connection == this) { - Slog.w(TAG, "Wallpaper service gone: " + mWallpaper.wallpaperComponent); - if (!mWallpaper.wallpaperUpdating - && (mWallpaper.lastDiedTime + MIN_WALLPAPER_CRASH_TIME) - > SystemClock.uptimeMillis() - && mWallpaper.userId == mCurrentUserId) { - Slog.w(TAG, "Reverting to built-in wallpaper!"); - clearWallpaperLocked(true, mWallpaper.userId, null); - } - } - } - } - - @Override - public void attachEngine(IWallpaperEngine engine) { - synchronized (mLock) { - mEngine = engine; - if (mDimensionsChanged) { - try { - mEngine.setDesiredSize(mWallpaper.width, mWallpaper.height); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to set wallpaper dimensions", e); - } - mDimensionsChanged = false; - } - } - } - - @Override - public void engineShown(IWallpaperEngine engine) { - synchronized (mLock) { - if (mReply != null) { - long ident = Binder.clearCallingIdentity(); - try { - mReply.sendResult(null); - } catch (RemoteException e) { - Binder.restoreCallingIdentity(ident); - } - mReply = null; - } - } - } - - @Override - public ParcelFileDescriptor setWallpaper(String name) { - synchronized (mLock) { - if (mWallpaper.connection == this) { - return updateWallpaperBitmapLocked(name, mWallpaper); - } - return null; - } - } - } - - class MyPackageMonitor extends PackageMonitor { - @Override - public void onPackageUpdateFinished(String packageName, int uid) { - synchronized (mLock) { - if (mCurrentUserId != getChangingUserId()) { - return; - } - WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); - if (wallpaper != null) { - if (wallpaper.wallpaperComponent != null - && wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { - wallpaper.wallpaperUpdating = false; - ComponentName comp = wallpaper.wallpaperComponent; - clearWallpaperComponentLocked(wallpaper); - if (!bindWallpaperComponentLocked(comp, false, false, - wallpaper, null)) { - Slog.w(TAG, "Wallpaper no longer available; reverting to default"); - clearWallpaperLocked(false, wallpaper.userId, null); - } - } - } - } - } - - @Override - public void onPackageModified(String packageName) { - synchronized (mLock) { - if (mCurrentUserId != getChangingUserId()) { - return; - } - WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); - if (wallpaper != null) { - if (wallpaper.wallpaperComponent == null - || !wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { - return; - } - doPackagesChangedLocked(true, wallpaper); - } - } - } - - @Override - public void onPackageUpdateStarted(String packageName, int uid) { - synchronized (mLock) { - if (mCurrentUserId != getChangingUserId()) { - return; - } - WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); - if (wallpaper != null) { - if (wallpaper.wallpaperComponent != null - && wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { - wallpaper.wallpaperUpdating = true; - } - } - } - } - - @Override - public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { - synchronized (mLock) { - boolean changed = false; - if (mCurrentUserId != getChangingUserId()) { - return false; - } - WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); - if (wallpaper != null) { - boolean res = doPackagesChangedLocked(doit, wallpaper); - changed |= res; - } - return changed; - } - } - - @Override - public void onSomePackagesChanged() { - synchronized (mLock) { - if (mCurrentUserId != getChangingUserId()) { - return; - } - WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); - if (wallpaper != null) { - doPackagesChangedLocked(true, wallpaper); - } - } - } - - boolean doPackagesChangedLocked(boolean doit, WallpaperData wallpaper) { - boolean changed = false; - if (wallpaper.wallpaperComponent != null) { - int change = isPackageDisappearing(wallpaper.wallpaperComponent - .getPackageName()); - if (change == PACKAGE_PERMANENT_CHANGE - || change == PACKAGE_TEMPORARY_CHANGE) { - changed = true; - if (doit) { - Slog.w(TAG, "Wallpaper uninstalled, removing: " - + wallpaper.wallpaperComponent); - clearWallpaperLocked(false, wallpaper.userId, null); - } - } - } - if (wallpaper.nextWallpaperComponent != null) { - int change = isPackageDisappearing(wallpaper.nextWallpaperComponent - .getPackageName()); - if (change == PACKAGE_PERMANENT_CHANGE - || change == PACKAGE_TEMPORARY_CHANGE) { - wallpaper.nextWallpaperComponent = null; - } - } - if (wallpaper.wallpaperComponent != null - && isPackageModified(wallpaper.wallpaperComponent.getPackageName())) { - try { - mContext.getPackageManager().getServiceInfo( - wallpaper.wallpaperComponent, 0); - } catch (NameNotFoundException e) { - Slog.w(TAG, "Wallpaper component gone, removing: " - + wallpaper.wallpaperComponent); - clearWallpaperLocked(false, wallpaper.userId, null); - } - } - if (wallpaper.nextWallpaperComponent != null - && isPackageModified(wallpaper.nextWallpaperComponent.getPackageName())) { - try { - mContext.getPackageManager().getServiceInfo( - wallpaper.nextWallpaperComponent, 0); - } catch (NameNotFoundException e) { - wallpaper.nextWallpaperComponent = null; - } - } - return changed; - } - } - - public WallpaperManagerService(Context context) { - if (DEBUG) Slog.v(TAG, "WallpaperService startup"); - mContext = context; - mIWindowManager = IWindowManager.Stub.asInterface( - ServiceManager.getService(Context.WINDOW_SERVICE)); - mIPackageManager = AppGlobals.getPackageManager(); - mMonitor = new MyPackageMonitor(); - mMonitor.register(context, null, UserHandle.ALL, true); - getWallpaperDir(UserHandle.USER_OWNER).mkdirs(); - loadSettingsLocked(UserHandle.USER_OWNER); - } - - private static File getWallpaperDir(int userId) { - return Environment.getUserSystemDirectory(userId); - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - for (int i = 0; i < mWallpaperMap.size(); i++) { - WallpaperData wallpaper = mWallpaperMap.valueAt(i); - wallpaper.wallpaperObserver.stopWatching(); - } - } - - public void systemRunning() { - if (DEBUG) Slog.v(TAG, "systemReady"); - WallpaperData wallpaper = mWallpaperMap.get(UserHandle.USER_OWNER); - switchWallpaper(wallpaper, null); - wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper); - wallpaper.wallpaperObserver.startWatching(); - - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_REMOVED); - userFilter.addAction(Intent.ACTION_USER_STOPPING); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_USER_REMOVED.equals(action)) { - onRemoveUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL)); - } - // TODO: Race condition causing problems when cleaning up on stopping a user. - // Comment this out for now. - // else if (Intent.ACTION_USER_STOPPING.equals(action)) { - // onStoppingUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - // UserHandle.USER_NULL)); - // } - } - }, userFilter); - - try { - ActivityManagerNative.getDefault().registerUserSwitchObserver( - new IUserSwitchObserver.Stub() { - @Override - public void onUserSwitching(int newUserId, IRemoteCallback reply) { - switchUser(newUserId, reply); - } - - @Override - public void onUserSwitchComplete(int newUserId) throws RemoteException { - } - }); - } catch (RemoteException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - String getName() { - synchronized (mLock) { - return mWallpaperMap.get(0).name; - } - } - - void onStoppingUser(int userId) { - if (userId < 1) return; - synchronized (mLock) { - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper != null) { - if (wallpaper.wallpaperObserver != null) { - wallpaper.wallpaperObserver.stopWatching(); - wallpaper.wallpaperObserver = null; - } - mWallpaperMap.remove(userId); - } - } - } - - void onRemoveUser(int userId) { - if (userId < 1) return; - synchronized (mLock) { - onStoppingUser(userId); - File wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER); - wallpaperFile.delete(); - File wallpaperInfoFile = new File(getWallpaperDir(userId), WALLPAPER_INFO); - wallpaperInfoFile.delete(); - } - } - - void switchUser(int userId, IRemoteCallback reply) { - synchronized (mLock) { - mCurrentUserId = userId; - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper == null) { - wallpaper = new WallpaperData(userId); - mWallpaperMap.put(userId, wallpaper); - loadSettingsLocked(userId); - } - // Not started watching yet, in case wallpaper data was loaded for other reasons. - if (wallpaper.wallpaperObserver == null) { - wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper); - wallpaper.wallpaperObserver.startWatching(); - } - switchWallpaper(wallpaper, reply); - } - } - - void switchWallpaper(WallpaperData wallpaper, IRemoteCallback reply) { - synchronized (mLock) { - RuntimeException e = null; - try { - ComponentName cname = wallpaper.wallpaperComponent != null ? - wallpaper.wallpaperComponent : wallpaper.nextWallpaperComponent; - if (bindWallpaperComponentLocked(cname, true, false, wallpaper, reply)) { - return; - } - } catch (RuntimeException e1) { - e = e1; - } - Slog.w(TAG, "Failure starting previous wallpaper", e); - clearWallpaperLocked(false, wallpaper.userId, reply); - } - } - - public void clearWallpaper() { - if (DEBUG) Slog.v(TAG, "clearWallpaper"); - synchronized (mLock) { - clearWallpaperLocked(false, UserHandle.getCallingUserId(), null); - } - } - - void clearWallpaperLocked(boolean defaultFailed, int userId, IRemoteCallback reply) { - WallpaperData wallpaper = mWallpaperMap.get(userId); - File f = new File(getWallpaperDir(userId), WALLPAPER); - if (f.exists()) { - f.delete(); - } - final long ident = Binder.clearCallingIdentity(); - RuntimeException e = null; - try { - wallpaper.imageWallpaperPending = false; - if (userId != mCurrentUserId) return; - if (bindWallpaperComponentLocked(defaultFailed - ? IMAGE_WALLPAPER - : null, true, false, wallpaper, reply)) { - return; - } - } catch (IllegalArgumentException e1) { - e = e1; - } finally { - Binder.restoreCallingIdentity(ident); - } - - // This can happen if the default wallpaper component doesn't - // exist. This should be a system configuration problem, but - // let's not let it crash the system and just live with no - // wallpaper. - Slog.e(TAG, "Default wallpaper component not found!", e); - clearWallpaperComponentLocked(wallpaper); - if (reply != null) { - try { - reply.sendResult(null); - } catch (RemoteException e1) { - } - } - } - - public boolean hasNamedWallpaper(String name) { - synchronized (mLock) { - List users; - long ident = Binder.clearCallingIdentity(); - try { - users = ((UserManager) mContext.getSystemService(Context.USER_SERVICE)).getUsers(); - } finally { - Binder.restoreCallingIdentity(ident); - } - for (UserInfo user: users) { - WallpaperData wd = mWallpaperMap.get(user.id); - if (wd == null) { - // User hasn't started yet, so load her settings to peek at the wallpaper - loadSettingsLocked(user.id); - wd = mWallpaperMap.get(user.id); - } - if (wd != null && name.equals(wd.name)) { - return true; - } - } - } - return false; - } - - private Point getDefaultDisplaySize() { - Point p = new Point(); - WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); - Display d = wm.getDefaultDisplay(); - d.getRealSize(p); - return p; - } - - public void setDimensionHints(int width, int height) throws RemoteException { - checkPermission(android.Manifest.permission.SET_WALLPAPER_HINTS); - synchronized (mLock) { - int userId = UserHandle.getCallingUserId(); - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper == null) { - throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); - } - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("width and height must be > 0"); - } - // Make sure it is at least as large as the display. - Point displaySize = getDefaultDisplaySize(); - width = Math.max(width, displaySize.x); - height = Math.max(height, displaySize.y); - - if (width != wallpaper.width || height != wallpaper.height) { - wallpaper.width = width; - wallpaper.height = height; - saveSettingsLocked(wallpaper); - if (mCurrentUserId != userId) return; // Don't change the properties now - if (wallpaper.connection != null) { - if (wallpaper.connection.mEngine != null) { - try { - wallpaper.connection.mEngine.setDesiredSize( - width, height); - } catch (RemoteException e) { - } - notifyCallbacksLocked(wallpaper); - } else if (wallpaper.connection.mService != null) { - // We've attached to the service but the engine hasn't attached back to us - // yet. This means it will be created with the previous dimensions, so we - // need to update it to the new dimensions once it attaches. - wallpaper.connection.mDimensionsChanged = true; - } - } - } - } - } - - public int getWidthHint() throws RemoteException { - synchronized (mLock) { - WallpaperData wallpaper = mWallpaperMap.get(UserHandle.getCallingUserId()); - return wallpaper.width; - } - } - - public int getHeightHint() throws RemoteException { - synchronized (mLock) { - WallpaperData wallpaper = mWallpaperMap.get(UserHandle.getCallingUserId()); - return wallpaper.height; - } - } - - public ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb, - Bundle outParams) { - synchronized (mLock) { - // This returns the current user's wallpaper, if called by a system service. Else it - // returns the wallpaper for the calling user. - int callingUid = Binder.getCallingUid(); - int wallpaperUserId = 0; - if (callingUid == android.os.Process.SYSTEM_UID) { - wallpaperUserId = mCurrentUserId; - } else { - wallpaperUserId = UserHandle.getUserId(callingUid); - } - WallpaperData wallpaper = mWallpaperMap.get(wallpaperUserId); - try { - if (outParams != null) { - outParams.putInt("width", wallpaper.width); - outParams.putInt("height", wallpaper.height); - } - wallpaper.callbacks.register(cb); - File f = new File(getWallpaperDir(wallpaperUserId), WALLPAPER); - if (!f.exists()) { - return null; - } - return ParcelFileDescriptor.open(f, MODE_READ_ONLY); - } catch (FileNotFoundException e) { - /* Shouldn't happen as we check to see if the file exists */ - Slog.w(TAG, "Error getting wallpaper", e); - } - return null; - } - } - - public WallpaperInfo getWallpaperInfo() { - int userId = UserHandle.getCallingUserId(); - synchronized (mLock) { - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper.connection != null) { - return wallpaper.connection.mInfo; - } - return null; - } - } - - public ParcelFileDescriptor setWallpaper(String name) { - checkPermission(android.Manifest.permission.SET_WALLPAPER); - synchronized (mLock) { - if (DEBUG) Slog.v(TAG, "setWallpaper"); - int userId = UserHandle.getCallingUserId(); - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper == null) { - throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); - } - final long ident = Binder.clearCallingIdentity(); - try { - ParcelFileDescriptor pfd = updateWallpaperBitmapLocked(name, wallpaper); - if (pfd != null) { - wallpaper.imageWallpaperPending = true; - } - return pfd; - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - - ParcelFileDescriptor updateWallpaperBitmapLocked(String name, WallpaperData wallpaper) { - if (name == null) name = ""; - try { - File dir = getWallpaperDir(wallpaper.userId); - if (!dir.exists()) { - dir.mkdir(); - FileUtils.setPermissions( - dir.getPath(), - FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, - -1, -1); - } - File file = new File(dir, WALLPAPER); - ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, - MODE_CREATE|MODE_READ_WRITE); - if (!SELinux.restorecon(file)) { - return null; - } - wallpaper.name = name; - return fd; - } catch (FileNotFoundException e) { - Slog.w(TAG, "Error setting wallpaper", e); - } - return null; - } - - public void setWallpaperComponent(ComponentName name) { - checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT); - synchronized (mLock) { - if (DEBUG) Slog.v(TAG, "setWallpaperComponent name=" + name); - int userId = UserHandle.getCallingUserId(); - WallpaperData wallpaper = mWallpaperMap.get(userId); - if (wallpaper == null) { - throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); - } - final long ident = Binder.clearCallingIdentity(); - try { - wallpaper.imageWallpaperPending = false; - bindWallpaperComponentLocked(name, false, true, wallpaper, null); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } - - boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force, - boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) { - if (DEBUG) Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName); - // Has the component changed? - if (!force) { - if (wallpaper.connection != null) { - if (wallpaper.wallpaperComponent == null) { - if (componentName == null) { - if (DEBUG) Slog.v(TAG, "bindWallpaperComponentLocked: still using default"); - // Still using default wallpaper. - return true; - } - } else if (wallpaper.wallpaperComponent.equals(componentName)) { - // Changing to same wallpaper. - if (DEBUG) Slog.v(TAG, "same wallpaper"); - return true; - } - } - } - - try { - if (componentName == null) { - String defaultComponent = - mContext.getString(com.android.internal.R.string.default_wallpaper_component); - if (defaultComponent != null) { - // See if there is a default wallpaper component specified - componentName = ComponentName.unflattenFromString(defaultComponent); - if (DEBUG) Slog.v(TAG, "Use default component wallpaper:" + componentName); - } - if (componentName == null) { - // Fall back to static image wallpaper - componentName = IMAGE_WALLPAPER; - //clearWallpaperComponentLocked(); - //return; - if (DEBUG) Slog.v(TAG, "Using image wallpaper"); - } - } - int serviceUserId = wallpaper.userId; - ServiceInfo si = mIPackageManager.getServiceInfo(componentName, - PackageManager.GET_META_DATA | PackageManager.GET_PERMISSIONS, serviceUserId); - if (si == null) { - // The wallpaper component we're trying to use doesn't exist - Slog.w(TAG, "Attempted wallpaper " + componentName + " is unavailable"); - return false; - } - if (!android.Manifest.permission.BIND_WALLPAPER.equals(si.permission)) { - String msg = "Selected service does not require " - + android.Manifest.permission.BIND_WALLPAPER - + ": " + componentName; - if (fromUser) { - throw new SecurityException(msg); - } - Slog.w(TAG, msg); - return false; - } - - WallpaperInfo wi = null; - - Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); - if (componentName != null && !componentName.equals(IMAGE_WALLPAPER)) { - // Make sure the selected service is actually a wallpaper service. - List ris = - mIPackageManager.queryIntentServices(intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - PackageManager.GET_META_DATA, serviceUserId); - for (int i=0; i 4 && "res:".equals(wallpaper.name.substring(0, 4))) { - String resName = wallpaper.name.substring(4); - - String pkg = null; - int colon = resName.indexOf(':'); - if (colon > 0) { - pkg = resName.substring(0, colon); - } - - String ident = null; - int slash = resName.lastIndexOf('/'); - if (slash > 0) { - ident = resName.substring(slash+1); - } - - String type = null; - if (colon > 0 && slash > 0 && (slash-colon) > 1) { - type = resName.substring(colon+1, slash); - } - - if (pkg != null && ident != null && type != null) { - int resId = -1; - InputStream res = null; - FileOutputStream fos = null; - try { - Context c = mContext.createPackageContext(pkg, Context.CONTEXT_RESTRICTED); - Resources r = c.getResources(); - resId = r.getIdentifier(resName, null, null); - if (resId == 0) { - Slog.e(TAG, "couldn't resolve identifier pkg=" + pkg + " type=" + type - + " ident=" + ident); - return false; - } - - res = r.openRawResource(resId); - if (wallpaper.wallpaperFile.exists()) { - wallpaper.wallpaperFile.delete(); - } - fos = new FileOutputStream(wallpaper.wallpaperFile); - - byte[] buffer = new byte[32768]; - int amt; - while ((amt=res.read(buffer)) > 0) { - fos.write(buffer, 0, amt); - } - // mWallpaperObserver will notice the close and send the change broadcast - - Slog.v(TAG, "Restored wallpaper: " + resName); - return true; - } catch (NameNotFoundException e) { - Slog.e(TAG, "Package name " + pkg + " not found"); - } catch (Resources.NotFoundException e) { - Slog.e(TAG, "Resource not found: " + resId); - } catch (IOException e) { - Slog.e(TAG, "IOException while restoring wallpaper ", e); - } finally { - if (res != null) { - try { - res.close(); - } catch (IOException ex) {} - } - if (fos != null) { - FileUtils.sync(fos); - try { - fos.close(); - } catch (IOException ex) {} - } - } - } - } - return false; - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) - != PackageManager.PERMISSION_GRANTED) { - - pw.println("Permission Denial: can't dump wallpaper service from from pid=" - + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return; - } - - synchronized (mLock) { - pw.println("Current Wallpaper Service state:"); - for (int i = 0; i < mWallpaperMap.size(); i++) { - WallpaperData wallpaper = mWallpaperMap.valueAt(i); - pw.println(" User " + wallpaper.userId + ":"); - pw.print(" mWidth="); - pw.print(wallpaper.width); - pw.print(" mHeight="); - pw.println(wallpaper.height); - pw.print(" mName="); - pw.println(wallpaper.name); - pw.print(" mWallpaperComponent="); - pw.println(wallpaper.wallpaperComponent); - if (wallpaper.connection != null) { - WallpaperConnection conn = wallpaper.connection; - pw.print(" Wallpaper connection "); - pw.print(conn); - pw.println(":"); - if (conn.mInfo != null) { - pw.print(" mInfo.component="); - pw.println(conn.mInfo.getComponent()); - } - pw.print(" mToken="); - pw.println(conn.mToken); - pw.print(" mService="); - pw.println(conn.mService); - pw.print(" mEngine="); - pw.println(conn.mEngine); - pw.print(" mLastDiedTime="); - pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis()); - } - } - } - } -} diff --git a/services/java/com/android/server/appwidget/AppWidgetService.java b/services/java/com/android/server/appwidget/AppWidgetService.java new file mode 100644 index 000000000000..6fd8871c2185 --- /dev/null +++ b/services/java/com/android/server/appwidget/AppWidgetService.java @@ -0,0 +1,363 @@ +/* + * 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 com.android.server.appwidget; + +import android.app.ActivityManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import com.android.internal.appwidget.IAppWidgetHost; +import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.Locale; + + +/** + * Redirects calls to this service to the instance of the service for the appropriate user. + */ +public class AppWidgetService extends IAppWidgetService.Stub +{ + private static final String TAG = "AppWidgetService"; + + Context mContext; + Locale mLocale; + PackageManager mPackageManager; + boolean mSafeMode; + private final Handler mSaveStateHandler; + + private final SparseArray mAppWidgetServices; + + public AppWidgetService(Context context) { + mContext = context; + + mSaveStateHandler = BackgroundThread.getHandler(); + + mAppWidgetServices = new SparseArray(5); + AppWidgetServiceImpl primary = new AppWidgetServiceImpl(context, 0, mSaveStateHandler); + mAppWidgetServices.append(0, primary); + } + + public void systemRunning(boolean safeMode) { + mSafeMode = safeMode; + + mAppWidgetServices.get(0).systemReady(safeMode); + + // Register for the boot completed broadcast, so we can send the + // ENABLE broacasts. If we try to send them now, they time out, + // because the system isn't ready to handle them yet. + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null); + + // Register for configuration changes so we can update the names + // of the widgets when the locale changes. + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED), null, null); + + // Register for broadcasts about package install, etc., so we can + // update the provider list. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + filter, null, null); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + sdFilter, null, null); + + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + userFilter.addAction(Intent.ACTION_USER_STOPPING); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) { + onUserRemoved(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL)); + } else if (Intent.ACTION_USER_STOPPING.equals(intent.getAction())) { + onUserStopping(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL)); + } + } + }, userFilter); + } + + @Override + public int allocateAppWidgetId(String packageName, int hostId, int userId) + throws RemoteException { + return getImplForUser(userId).allocateAppWidgetId(packageName, hostId); + } + + @Override + public int[] getAppWidgetIdsForHost(int hostId, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetIdsForHost(hostId); + } + + @Override + public void deleteAppWidgetId(int appWidgetId, int userId) throws RemoteException { + getImplForUser(userId).deleteAppWidgetId(appWidgetId); + } + + @Override + public void deleteHost(int hostId, int userId) throws RemoteException { + getImplForUser(userId).deleteHost(hostId); + } + + @Override + public void deleteAllHosts(int userId) throws RemoteException { + getImplForUser(userId).deleteAllHosts(); + } + + @Override + public void bindAppWidgetId(int appWidgetId, ComponentName provider, Bundle options, int userId) + throws RemoteException { + getImplForUser(userId).bindAppWidgetId(appWidgetId, provider, options); + } + + @Override + public boolean bindAppWidgetIdIfAllowed( + String packageName, int appWidgetId, ComponentName provider, Bundle options, int userId) + throws RemoteException { + return getImplForUser(userId).bindAppWidgetIdIfAllowed( + packageName, appWidgetId, provider, options); + } + + @Override + public boolean hasBindAppWidgetPermission(String packageName, int userId) + throws RemoteException { + return getImplForUser(userId).hasBindAppWidgetPermission(packageName); + } + + @Override + public void setBindAppWidgetPermission(String packageName, boolean permission, int userId) + throws RemoteException { + getImplForUser(userId).setBindAppWidgetPermission(packageName, permission); + } + + @Override + public void bindRemoteViewsService(int appWidgetId, Intent intent, IBinder connection, + int userId) throws RemoteException { + getImplForUser(userId).bindRemoteViewsService(appWidgetId, intent, connection); + } + + @Override + public int[] startListening(IAppWidgetHost host, String packageName, int hostId, + List updatedViews, int userId) throws RemoteException { + return getImplForUser(userId).startListening(host, packageName, hostId, updatedViews); + } + + public void onUserRemoved(int userId) { + if (userId < 1) return; + synchronized (mAppWidgetServices) { + AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); + mAppWidgetServices.remove(userId); + + if (impl == null) { + AppWidgetServiceImpl.getSettingsFile(userId).delete(); + } else { + impl.onUserRemoved(); + } + } + } + + public void onUserStopping(int userId) { + if (userId < 1) return; + synchronized (mAppWidgetServices) { + AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); + if (impl != null) { + mAppWidgetServices.remove(userId); + impl.onUserStopping(); + } + } + } + + private void checkPermission(int userId) { + int realUserId = ActivityManager.handleIncomingUser( + Binder.getCallingPid(), + Binder.getCallingUid(), + userId, + false, /* allowAll */ + true, /* requireFull */ + this.getClass().getSimpleName(), + this.getClass().getPackage().getName()); + } + + private AppWidgetServiceImpl getImplForUser(int userId) { + checkPermission(userId); + boolean sendInitial = false; + AppWidgetServiceImpl service; + synchronized (mAppWidgetServices) { + service = mAppWidgetServices.get(userId); + if (service == null) { + Slog.i(TAG, "Unable to find AppWidgetServiceImpl for user " + userId + ", adding"); + // TODO: Verify that it's a valid user + service = new AppWidgetServiceImpl(mContext, userId, mSaveStateHandler); + service.systemReady(mSafeMode); + // Assume that BOOT_COMPLETED was received, as this is a non-primary user. + mAppWidgetServices.append(userId, service); + sendInitial = true; + } + } + if (sendInitial) { + service.sendInitialBroadcasts(); + } + return service; + } + + @Override + public int[] getAppWidgetIds(ComponentName provider, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetIds(provider); + } + + @Override + public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId, int userId) + throws RemoteException { + return getImplForUser(userId).getAppWidgetInfo(appWidgetId); + } + + @Override + public RemoteViews getAppWidgetViews(int appWidgetId, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetViews(appWidgetId); + } + + @Override + public void updateAppWidgetOptions(int appWidgetId, Bundle options, int userId) { + getImplForUser(userId).updateAppWidgetOptions(appWidgetId, options); + } + + @Override + public Bundle getAppWidgetOptions(int appWidgetId, int userId) { + return getImplForUser(userId).getAppWidgetOptions(appWidgetId); + } + + @Override + public List getInstalledProviders(int categoryFilter, int userId) + throws RemoteException { + return getImplForUser(userId).getInstalledProviders(categoryFilter); + } + + @Override + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId, int userId) + throws RemoteException { + getImplForUser(userId).notifyAppWidgetViewDataChanged( + appWidgetIds, viewId); + } + + @Override + public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).partiallyUpdateAppWidgetIds( + appWidgetIds, views); + } + + @Override + public void stopListening(int hostId, int userId) throws RemoteException { + getImplForUser(userId).stopListening(hostId); + } + + @Override + public void unbindRemoteViewsService(int appWidgetId, Intent intent, int userId) + throws RemoteException { + getImplForUser(userId).unbindRemoteViewsService( + appWidgetId, intent); + } + + @Override + public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).updateAppWidgetIds(appWidgetIds, views); + } + + @Override + public void updateAppWidgetProvider(ComponentName provider, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).updateAppWidgetProvider(provider, views); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + // Dump the state of all the app widget providers + synchronized (mAppWidgetServices) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + for (int i = 0; i < mAppWidgetServices.size(); i++) { + pw.println("User: " + mAppWidgetServices.keyAt(i)); + ipw.increaseIndent(); + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.dump(fd, ipw, args); + ipw.decreaseIndent(); + } + } + } + + BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + // Slog.d(TAG, "received " + action); + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId >= 0) { + getImplForUser(userId).sendInitialBroadcasts(); + } else { + Slog.w(TAG, "Incorrect user handle supplied in " + intent); + } + } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { + for (int i = 0; i < mAppWidgetServices.size(); i++) { + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.onConfigurationChanged(); + } + } else { + int sendingUser = getSendingUserId(); + if (sendingUser == UserHandle.USER_ALL) { + for (int i = 0; i < mAppWidgetServices.size(); i++) { + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.onBroadcastReceived(intent); + } + } else { + AppWidgetServiceImpl service = mAppWidgetServices.get(sendingUser); + if (service != null) { + service.onBroadcastReceived(intent); + } + } + } + } + }; +} diff --git a/services/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/java/com/android/server/appwidget/AppWidgetServiceImpl.java new file mode 100644 index 000000000000..98dead312469 --- /dev/null +++ b/services/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -0,0 +1,2126 @@ +/* + * 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 com.android.server.appwidget; + +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.Intent.FilterComparison; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Point; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.AtomicFile; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.TypedValue; +import android.util.Xml; +import android.view.Display; +import android.view.WindowManager; +import android.widget.RemoteViews; + +import com.android.internal.appwidget.IAppWidgetHost; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.widget.IRemoteViewsAdapterConnection; +import com.android.internal.widget.IRemoteViewsFactory; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +class AppWidgetServiceImpl { + + private static final String KEYGUARD_HOST_PACKAGE = "com.android.keyguard"; + private static final int KEYGUARD_HOST_ID = 0x4b455947; + private static final String TAG = "AppWidgetServiceImpl"; + private static final String SETTINGS_FILENAME = "appwidgets.xml"; + private static final int MIN_UPDATE_PERIOD = 30 * 60 * 1000; // 30 minutes + private static final int CURRENT_VERSION = 1; // Bump if the stored widgets need to be upgraded. + + private static boolean DBG = false; + + /* + * When identifying a Host or Provider based on the calling process, use the uid field. When + * identifying a Host or Provider based on a package manager broadcast, use the package given. + */ + + static class Provider { + int uid; + AppWidgetProviderInfo info; + ArrayList instances = new ArrayList(); + PendingIntent broadcast; + boolean zombie; // if we're in safe mode, don't prune this just because nobody references it + + int tag; // for use while saving state (the index) + } + + static class Host { + int uid; + int hostId; + String packageName; + ArrayList instances = new ArrayList(); + IAppWidgetHost callbacks; + boolean zombie; // if we're in safe mode, don't prune this just because nobody references it + + int tag; // for use while saving state (the index) + + boolean uidMatches(int callingUid) { + if (UserHandle.getAppId(callingUid) == Process.myUid()) { + // For a host that's in the system process, ignore the user id + return UserHandle.isSameApp(this.uid, callingUid); + } else { + return this.uid == callingUid; + } + } + } + + static class AppWidgetId { + int appWidgetId; + Provider provider; + RemoteViews views; + Bundle options; + Host host; + } + + /** + * Acts as a proxy between the ServiceConnection and the RemoteViewsAdapterConnection. This + * needs to be a static inner class since a reference to the ServiceConnection is held globally + * and may lead us to leak AppWidgetService instances (if there were more than one). + */ + static class ServiceConnectionProxy implements ServiceConnection { + private final IBinder mConnectionCb; + + ServiceConnectionProxy(Pair key, IBinder connectionCb) { + mConnectionCb = connectionCb; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub + .asInterface(mConnectionCb); + try { + cb.onServiceConnected(service); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void onServiceDisconnected(ComponentName name) { + disconnect(); + } + + public void disconnect() { + final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub + .asInterface(mConnectionCb); + try { + cb.onServiceDisconnected(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + // Manages active connections to RemoteViewsServices + private final HashMap, ServiceConnection> mBoundRemoteViewsServices = new HashMap, ServiceConnection>(); + // Manages persistent references to RemoteViewsServices from different App Widgets + private final HashMap> mRemoteViewsServicesAppWidgets = new HashMap>(); + + final Context mContext; + final IPackageManager mPm; + final AlarmManager mAlarmManager; + final ArrayList mInstalledProviders = new ArrayList(); + final int mUserId; + final boolean mHasFeature; + + Locale mLocale; + int mNextAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + 1; + final ArrayList mAppWidgetIds = new ArrayList(); + final ArrayList mHosts = new ArrayList(); + // set of package names + final HashSet mPackagesWithBindWidgetPermission = new HashSet(); + boolean mSafeMode; + boolean mStateLoaded; + int mMaxWidgetBitmapMemory; + + private final Handler mSaveStateHandler; + + // These are for debugging only -- widgets are going missing in some rare instances + ArrayList mDeletedProviders = new ArrayList(); + ArrayList mDeletedHosts = new ArrayList(); + + AppWidgetServiceImpl(Context context, int userId, Handler saveStateHandler) { + mContext = context; + mPm = AppGlobals.getPackageManager(); + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mUserId = userId; + mSaveStateHandler = saveStateHandler; + mHasFeature = context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_APP_WIDGETS); + computeMaximumWidgetBitmapMemory(); + } + + void computeMaximumWidgetBitmapMemory() { + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getRealSize(size); + // Cap memory usage at 1.5 times the size of the display + // 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h + mMaxWidgetBitmapMemory = 6 * size.x * size.y; + } + + public void systemReady(boolean safeMode) { + mSafeMode = safeMode; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + } + } + + private void log(String msg) { + Slog.i(TAG, "u=" + mUserId + ": " + msg); + } + + void onConfigurationChanged() { + if (DBG) log("Got onConfigurationChanged()"); + Locale revised = Locale.getDefault(); + if (revised == null || mLocale == null || !(revised.equals(mLocale))) { + mLocale = revised; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + // Note: updateProvidersForPackageLocked() may remove providers, so we must copy the + // list of installed providers and skip providers that we don't need to update. + // Also note that remove the provider does not clear the Provider component data. + ArrayList installedProviders = + new ArrayList(mInstalledProviders); + HashSet removedProviders = new HashSet(); + int N = installedProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = installedProviders.get(i); + ComponentName cn = p.info.provider; + if (!removedProviders.contains(cn)) { + updateProvidersForPackageLocked(cn.getPackageName(), removedProviders); + } + } + saveStateAsync(); + } + } + } + + void onBroadcastReceived(Intent intent) { + if (DBG) log("onBroadcast " + intent); + final String action = intent.getAction(); + boolean added = false; + boolean changed = false; + boolean providersModified = false; + String pkgList[] = null; + if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + added = true; + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + added = false; + } else { + Uri uri = intent.getData(); + if (uri == null) { + return; + } + String pkgName = uri.getSchemeSpecificPart(); + if (pkgName == null) { + return; + } + pkgList = new String[] { pkgName }; + added = Intent.ACTION_PACKAGE_ADDED.equals(action); + changed = Intent.ACTION_PACKAGE_CHANGED.equals(action); + } + if (pkgList == null || pkgList.length == 0) { + return; + } + if (added || changed) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Bundle extras = intent.getExtras(); + if (changed + || (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false))) { + for (String pkgName : pkgList) { + // The package was just upgraded + providersModified |= updateProvidersForPackageLocked(pkgName, null); + } + } else { + // The package was just added + for (String pkgName : pkgList) { + providersModified |= addProvidersForPackageLocked(pkgName); + } + } + saveStateAsync(); + } + } else { + Bundle extras = intent.getExtras(); + if (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false)) { + // The package is being updated. We'll receive a PACKAGE_ADDED shortly. + } else { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (String pkgName : pkgList) { + providersModified |= removeProvidersForPackageLocked(pkgName); + saveStateAsync(); + } + } + } + } + + if (providersModified) { + // If the set of providers has been modified, notify each active AppWidgetHost + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + notifyHostsForProvidersChangedLocked(); + } + } + } + + private void dumpProvider(Provider p, int index, PrintWriter pw) { + AppWidgetProviderInfo info = p.info; + pw.print(" ["); pw.print(index); pw.print("] provider "); + pw.print(info.provider.flattenToShortString()); + pw.println(':'); + pw.print(" min=("); pw.print(info.minWidth); + pw.print("x"); pw.print(info.minHeight); + pw.print(") minResize=("); pw.print(info.minResizeWidth); + pw.print("x"); pw.print(info.minResizeHeight); + pw.print(") updatePeriodMillis="); + pw.print(info.updatePeriodMillis); + pw.print(" resizeMode="); + pw.print(info.resizeMode); + pw.print(info.widgetCategory); + pw.print(" autoAdvanceViewId="); + pw.print(info.autoAdvanceViewId); + pw.print(" initialLayout=#"); + pw.print(Integer.toHexString(info.initialLayout)); + pw.print(" uid="); pw.print(p.uid); + pw.print(" zombie="); pw.println(p.zombie); + } + + private void dumpHost(Host host, int index, PrintWriter pw) { + pw.print(" ["); pw.print(index); pw.print("] hostId="); + pw.print(host.hostId); pw.print(' '); + pw.print(host.packageName); pw.print('/'); + pw.print(host.uid); pw.println(':'); + pw.print(" callbacks="); pw.println(host.callbacks); + pw.print(" instances.size="); pw.print(host.instances.size()); + pw.print(" zombie="); pw.println(host.zombie); + } + + private void dumpAppWidgetId(AppWidgetId id, int index, PrintWriter pw) { + pw.print(" ["); pw.print(index); pw.print("] id="); + pw.println(id.appWidgetId); + pw.print(" hostId="); + pw.print(id.host.hostId); pw.print(' '); + pw.print(id.host.packageName); pw.print('/'); + pw.println(id.host.uid); + if (id.provider != null) { + pw.print(" provider="); + pw.println(id.provider.info.provider.flattenToShortString()); + } + if (id.host != null) { + pw.print(" host.callbacks="); pw.println(id.host.callbacks); + } + if (id.views != null) { + pw.print(" views="); pw.println(id.views); + } + } + + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mAppWidgetIds) { + int N = mInstalledProviders.size(); + pw.println("Providers:"); + for (int i=0; i key = Pair.create(appWidgetId, fc); + if (mBoundRemoteViewsServices.containsKey(key)) { + conn = (ServiceConnectionProxy) mBoundRemoteViewsServices.get(key); + conn.disconnect(); + mContext.unbindService(conn); + mBoundRemoteViewsServices.remove(key); + } + + int userId = UserHandle.getUserId(id.provider.uid); + if (userId != mUserId) { + Slog.w(TAG, "AppWidgetServiceImpl of user " + mUserId + + " binding to provider on user " + userId); + } + // Bind to the RemoteViewsService (which will trigger a callback to the + // RemoteViewsAdapter.onServiceConnected()) + final long token = Binder.clearCallingIdentity(); + try { + conn = new ServiceConnectionProxy(key, connection); + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + mBoundRemoteViewsServices.put(key, conn); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Add it to the mapping of RemoteViewsService to appWidgetIds so that we can determine + // when we can call back to the RemoteViewsService later to destroy associated + // factories. + incrementAppWidgetServiceRefCount(appWidgetId, fc); + } + } + + // Unbinds from a specific RemoteViewsService + public void unbindRemoteViewsService(int appWidgetId, Intent intent) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + ensureStateLoadedLocked(); + // Unbind from the RemoteViewsService (which will trigger a callback to the bound + // RemoteViewsAdapter) + Pair key = Pair.create(appWidgetId, new FilterComparison( + intent)); + if (mBoundRemoteViewsServices.containsKey(key)) { + // We don't need to use the appWidgetId until after we are sure there is something + // to unbind. Note that this may mask certain issues with apps calling unbind() + // more than necessary. + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id == null) { + throw new IllegalArgumentException("bad appWidgetId"); + } + + ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices + .get(key); + conn.disconnect(); + mContext.unbindService(conn); + mBoundRemoteViewsServices.remove(key); + } + } + } + + // Unbinds from a RemoteViewsService when we delete an app widget + private void unbindAppWidgetRemoteViewsServicesLocked(AppWidgetId id) { + int appWidgetId = id.appWidgetId; + // Unbind all connections to Services bound to this AppWidgetId + Iterator> it = mBoundRemoteViewsServices.keySet() + .iterator(); + while (it.hasNext()) { + final Pair key = it.next(); + if (key.first.intValue() == appWidgetId) { + final ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices + .get(key); + conn.disconnect(); + mContext.unbindService(conn); + it.remove(); + } + } + + // Check if we need to destroy any services (if no other app widgets are + // referencing the same service) + decrementAppWidgetServiceRefCount(id); + } + + // Destroys the cached factory on the RemoteViewsService's side related to the specified intent + private void destroyRemoteViewsService(final Intent intent, AppWidgetId id) { + final ServiceConnection conn = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + final IRemoteViewsFactory cb = IRemoteViewsFactory.Stub.asInterface(service); + try { + cb.onDestroy(intent); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + mContext.unbindService(this); + } + + @Override + public void onServiceDisconnected(android.content.ComponentName name) { + // Do nothing + } + }; + + int userId = UserHandle.getUserId(id.provider.uid); + // Bind to the service and remove the static intent->factory mapping in the + // RemoteViewsService. + final long token = Binder.clearCallingIdentity(); + try { + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Adds to the ref-count for a given RemoteViewsService intent + private void incrementAppWidgetServiceRefCount(int appWidgetId, FilterComparison fc) { + HashSet appWidgetIds = null; + if (mRemoteViewsServicesAppWidgets.containsKey(fc)) { + appWidgetIds = mRemoteViewsServicesAppWidgets.get(fc); + } else { + appWidgetIds = new HashSet(); + mRemoteViewsServicesAppWidgets.put(fc, appWidgetIds); + } + appWidgetIds.add(appWidgetId); + } + + // Subtracts from the ref-count for a given RemoteViewsService intent, prompting a delete if + // the ref-count reaches zero. + private void decrementAppWidgetServiceRefCount(AppWidgetId id) { + Iterator it = mRemoteViewsServicesAppWidgets.keySet().iterator(); + while (it.hasNext()) { + final FilterComparison key = it.next(); + final HashSet ids = mRemoteViewsServicesAppWidgets.get(key); + if (ids.remove(id.appWidgetId)) { + // If we have removed the last app widget referencing this service, then we + // should destroy it and remove it from this set + if (ids.isEmpty()) { + destroyRemoteViewsService(key.getIntent(), id); + it.remove(); + } + } + } + } + + public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return null; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null && id.provider != null && !id.provider.zombie) { + return cloneIfLocalBinder(id.provider.info); + } + return null; + } + } + + public RemoteViews getAppWidgetViews(int appWidgetId) { + if (DBG) log("getAppWidgetViews id=" + appWidgetId); + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return null; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null) { + return cloneIfLocalBinder(id.views); + } + if (DBG) log(" couldn't find appwidgetid"); + return null; + } + } + + public List getInstalledProviders(int categoryFilter) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return new ArrayList(0); + } + ensureStateLoadedLocked(); + final int N = mInstalledProviders.size(); + ArrayList result = new ArrayList(N); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (!p.zombie && (p.info.widgetCategory & categoryFilter) != 0) { + result.add(cloneIfLocalBinder(p.info)); + } + } + return result; + } + } + + public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (DBG) log("updateAppWidgetIds views: " + views); + int bitmapMemoryUsage = 0; + if (views != null) { + bitmapMemoryUsage = views.estimateMemoryUsage(); + } + if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) { + throw new IllegalArgumentException("RemoteViews for widget update exceeds maximum" + + " bitmap memory usage (used: " + bitmapMemoryUsage + ", max: " + + mMaxWidgetBitmapMemory + ") The total memory cannot exceed that required to" + + " fill the device's screen once."); + } + + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + updateAppWidgetInstanceLocked(id, views); + } + } + } + + private void saveStateAsync() { + mSaveStateHandler.post(mSaveStateRunnable); + } + + private final Runnable mSaveStateRunnable = new Runnable() { + @Override + public void run() { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + saveStateLocked(); + } + } + }; + + public void updateAppWidgetOptions(int appWidgetId, Bundle options) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + options = cloneIfLocalBinder(options); + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + + if (id == null) { + return; + } + + Provider p = id.provider; + // Merge the options + id.options.putAll(options); + + // send the broacast saying that this appWidgetId has been deleted + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED); + intent.setComponent(p.info.provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id.appWidgetId); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, id.options); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + saveStateAsync(); + } + } + + public Bundle getAppWidgetOptions(int appWidgetId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return Bundle.EMPTY; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null && id.options != null) { + return cloneIfLocalBinder(id.options); + } else { + return Bundle.EMPTY; + } + } + } + + public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + if (id == null) { + Slog.w(TAG, "widget id " + appWidgetIds[i] + " not found!"); + } else if (id.views != null) { + // Only trigger a partial update for a widget if it has received a full update + updateAppWidgetInstanceLocked(id, views, true); + } + } + } + } + + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + notifyAppWidgetViewDataChangedInstanceLocked(id, viewId); + } + } + } + + public void updateAppWidgetProvider(ComponentName provider, RemoteViews views) { + if (!mHasFeature) { + return; + } + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Provider p = lookupProviderLocked(provider); + if (p == null) { + Slog.w(TAG, "updateAppWidgetProvider: provider doesn't exist: " + provider); + return; + } + ArrayList instances = p.instances; + final int callingUid = Binder.getCallingUid(); + final int N = instances.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = instances.get(i); + if (canAccessAppWidgetId(id, callingUid)) { + updateAppWidgetInstanceLocked(id, views); + } + } + } + } + + void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views) { + updateAppWidgetInstanceLocked(id, views, false); + } + + void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views, boolean isPartialUpdate) { + // allow for stale appWidgetIds and other badness + // lookup also checks that the calling process can access the appWidgetId + // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) + if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { + + if (!isPartialUpdate) { + // For a full update we replace the RemoteViews completely. + id.views = views; + } else { + // For a partial update, we merge the new RemoteViews with the old. + id.views.mergeRemoteViews(views); + } + + // is anyone listening? + if (id.host.callbacks != null) { + try { + // the lock is held, but this is a oneway call + id.host.callbacks.updateAppWidget(id.appWidgetId, views, mUserId); + } catch (RemoteException e) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this instance. + id.host.callbacks = null; + } + } + } + } + + void notifyAppWidgetViewDataChangedInstanceLocked(AppWidgetId id, int viewId) { + // allow for stale appWidgetIds and other badness + // lookup also checks that the calling process can access the appWidgetId + // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) + if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { + // is anyone listening? + if (id.host.callbacks != null) { + try { + // the lock is held, but this is a oneway call + id.host.callbacks.viewDataChanged(id.appWidgetId, viewId, mUserId); + } catch (RemoteException e) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this instance. + id.host.callbacks = null; + } + } + + // If the host is unavailable, then we call the associated + // RemoteViewsFactory.onDataSetChanged() directly + if (id.host.callbacks == null) { + Set keys = mRemoteViewsServicesAppWidgets.keySet(); + for (FilterComparison key : keys) { + if (mRemoteViewsServicesAppWidgets.get(key).contains(id.appWidgetId)) { + Intent intent = key.getIntent(); + + final ServiceConnection conn = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + IRemoteViewsFactory cb = IRemoteViewsFactory.Stub + .asInterface(service); + try { + cb.onDataSetChangedAsync(); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + mContext.unbindService(this); + } + + @Override + public void onServiceDisconnected(android.content.ComponentName name) { + // Do nothing + } + }; + + int userId = UserHandle.getUserId(id.provider.uid); + // Bind to the service and call onDataSetChanged() + final long token = Binder.clearCallingIdentity(); + try { + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + } + } + } + + private boolean isLocalBinder() { + return Process.myPid() == Binder.getCallingPid(); + } + + private RemoteViews cloneIfLocalBinder(RemoteViews rv) { + if (isLocalBinder() && rv != null) { + return rv.clone(); + } + return rv; + } + + private AppWidgetProviderInfo cloneIfLocalBinder(AppWidgetProviderInfo info) { + if (isLocalBinder() && info != null) { + return info.clone(); + } + return info; + } + + private Bundle cloneIfLocalBinder(Bundle bundle) { + // Note: this is only a shallow copy. For now this will be fine, but it could be problematic + // if we start adding objects to the options. Further, it would only be an issue if keyguard + // used such options. + if (isLocalBinder() && bundle != null) { + return (Bundle) bundle.clone(); + } + return bundle; + } + + public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId, + List updatedViews) { + if (!mHasFeature) { + return new int[0]; + } + int callingUid = enforceCallingUid(packageName); + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Host host = lookupOrAddHostLocked(callingUid, packageName, hostId); + host.callbacks = callbacks; + + updatedViews.clear(); + + ArrayList instances = host.instances; + int N = instances.size(); + int[] updatedIds = new int[N]; + for (int i = 0; i < N; i++) { + AppWidgetId id = instances.get(i); + updatedIds[i] = id.appWidgetId; + updatedViews.add(cloneIfLocalBinder(id.views)); + } + return updatedIds; + } + } + + public void stopListening(int hostId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + ensureStateLoadedLocked(); + Host host = lookupHostLocked(Binder.getCallingUid(), hostId); + if (host != null) { + host.callbacks = null; + pruneHostLocked(host); + } + } + } + + boolean canAccessAppWidgetId(AppWidgetId id, int callingUid) { + if (id.host.uidMatches(callingUid)) { + // Apps hosting the AppWidget have access to it. + return true; + } + if (id.provider != null && id.provider.uid == callingUid) { + // Apps providing the AppWidget have access to it (if the appWidgetId has been bound) + return true; + } + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.BIND_APPWIDGET) == PackageManager.PERMISSION_GRANTED) { + // Apps that can bind have access to all appWidgetIds. + return true; + } + // Nobody else can access it. + return false; + } + + AppWidgetId lookupAppWidgetIdLocked(int appWidgetId) { + int callingUid = Binder.getCallingUid(); + final int N = mAppWidgetIds.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = mAppWidgetIds.get(i); + if (id.appWidgetId == appWidgetId && canAccessAppWidgetId(id, callingUid)) { + return id; + } + } + return null; + } + + Provider lookupProviderLocked(ComponentName provider) { + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.info.provider.equals(provider)) { + return p; + } + } + return null; + } + + Host lookupHostLocked(int uid, int hostId) { + final int N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host h = mHosts.get(i); + if (h.uidMatches(uid) && h.hostId == hostId) { + return h; + } + } + return null; + } + + Host lookupOrAddHostLocked(int uid, String packageName, int hostId) { + final int N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host h = mHosts.get(i); + if (h.hostId == hostId && h.packageName.equals(packageName)) { + return h; + } + } + Host host = new Host(); + host.packageName = packageName; + host.uid = uid; + host.hostId = hostId; + mHosts.add(host); + return host; + } + + void pruneHostLocked(Host host) { + if (host.instances.size() == 0 && host.callbacks == null) { + mHosts.remove(host); + } + } + + void loadAppWidgetListLocked() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + try { + List broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + + final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + addProviderLocked(ri); + } + } catch (RemoteException re) { + // Shouldn't happen, local call + } + } + + boolean addProviderLocked(ResolveInfo ri) { + if ((ri.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + return false; + } + if (!ri.activityInfo.isEnabled()) { + return false; + } + Provider p = parseProviderInfoXml(new ComponentName(ri.activityInfo.packageName, + ri.activityInfo.name), ri); + if (p != null) { + mInstalledProviders.add(p); + return true; + } else { + return false; + } + } + + void removeProviderLocked(int index, Provider p) { + int N = p.instances.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = p.instances.get(i); + // Call back with empty RemoteViews + updateAppWidgetInstanceLocked(id, null); + // Stop telling the host about updates for this from now on + cancelBroadcasts(p); + // clear out references to this appWidgetId + id.host.instances.remove(id); + mAppWidgetIds.remove(id); + id.provider = null; + pruneHostLocked(id.host); + id.host = null; + } + p.instances.clear(); + mInstalledProviders.remove(index); + mDeletedProviders.add(p); + // no need to send the DISABLE broadcast, since the receiver is gone anyway + cancelBroadcasts(p); + } + + void sendEnableIntentLocked(Provider p) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED); + intent.setComponent(p.info.provider); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + } + + void sendUpdateIntentLocked(Provider p, int[] appWidgetIds) { + if (appWidgetIds != null && appWidgetIds.length > 0) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); + intent.setComponent(p.info.provider); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + } + } + + void registerForBroadcastsLocked(Provider p, int[] appWidgetIds) { + if (p.info.updatePeriodMillis > 0) { + // if this is the first instance, set the alarm. otherwise, + // rely on the fact that we've already set it and that + // PendingIntent.getBroadcast will update the extras. + boolean alreadyRegistered = p.broadcast != null; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); + intent.setComponent(p.info.provider); + long token = Binder.clearCallingIdentity(); + try { + p.broadcast = PendingIntent.getBroadcastAsUser(mContext, 1, intent, + PendingIntent.FLAG_UPDATE_CURRENT, new UserHandle(mUserId)); + } finally { + Binder.restoreCallingIdentity(token); + } + if (!alreadyRegistered) { + long period = p.info.updatePeriodMillis; + if (period < MIN_UPDATE_PERIOD) { + period = MIN_UPDATE_PERIOD; + } + mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock + .elapsedRealtime() + + period, period, p.broadcast); + } + } + } + + static int[] getAppWidgetIds(Provider p) { + int instancesSize = p.instances.size(); + int appWidgetIds[] = new int[instancesSize]; + for (int i = 0; i < instancesSize; i++) { + appWidgetIds[i] = p.instances.get(i).appWidgetId; + } + return appWidgetIds; + } + + public int[] getAppWidgetIds(ComponentName provider) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Provider p = lookupProviderLocked(provider); + if (p != null && Binder.getCallingUid() == p.uid) { + return getAppWidgetIds(p); + } else { + return new int[0]; + } + } + } + + static int[] getAppWidgetIds(Host h) { + int instancesSize = h.instances.size(); + int appWidgetIds[] = new int[instancesSize]; + for (int i = 0; i < instancesSize; i++) { + appWidgetIds[i] = h.instances.get(i).appWidgetId; + } + return appWidgetIds; + } + + public int[] getAppWidgetIdsForHost(int hostId) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + int callingUid = Binder.getCallingUid(); + Host host = lookupHostLocked(callingUid, hostId); + if (host != null) { + return getAppWidgetIds(host); + } else { + return new int[0]; + } + } + } + + private Provider parseProviderInfoXml(ComponentName component, ResolveInfo ri) { + Provider p = null; + + ActivityInfo activityInfo = ri.activityInfo; + XmlResourceParser parser = null; + try { + parser = activityInfo.loadXmlMetaData(mContext.getPackageManager(), + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER); + if (parser == null) { + Slog.w(TAG, "No " + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER + + " meta-data for " + "AppWidget provider '" + component + '\''); + return null; + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // drain whitespace, comments, etc. + } + + String nodeName = parser.getName(); + if (!"appwidget-provider".equals(nodeName)) { + Slog.w(TAG, "Meta-data does not start with appwidget-provider tag for" + + " AppWidget provider '" + component + '\''); + return null; + } + + p = new Provider(); + AppWidgetProviderInfo info = p.info = new AppWidgetProviderInfo(); + info.provider = component; + p.uid = activityInfo.applicationInfo.uid; + + Resources res = mContext.getPackageManager() + .getResourcesForApplicationAsUser(activityInfo.packageName, mUserId); + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AppWidgetProviderInfo); + + // These dimensions has to be resolved in the application's context. + // We simply send back the raw complex data, which will be + // converted to dp in {@link AppWidgetManager#getAppWidgetInfo}. + TypedValue value = sa + .peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minWidth); + info.minWidth = value != null ? value.data : 0; + value = sa.peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minHeight); + info.minHeight = value != null ? value.data : 0; + value = sa.peekValue( + com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeWidth); + info.minResizeWidth = value != null ? value.data : info.minWidth; + value = sa.peekValue( + com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeHeight); + info.minResizeHeight = value != null ? value.data : info.minHeight; + info.updatePeriodMillis = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_updatePeriodMillis, 0); + info.initialLayout = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_initialLayout, 0); + info.initialKeyguardLayout = sa.getResourceId(com.android.internal.R.styleable. + AppWidgetProviderInfo_initialKeyguardLayout, 0); + String className = sa + .getString(com.android.internal.R.styleable.AppWidgetProviderInfo_configure); + if (className != null) { + info.configure = new ComponentName(component.getPackageName(), className); + } + info.label = activityInfo.loadLabel(mContext.getPackageManager()).toString(); + info.icon = ri.getIconResource(); + info.previewImage = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_previewImage, 0); + info.autoAdvanceViewId = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_autoAdvanceViewId, -1); + info.resizeMode = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_resizeMode, + AppWidgetProviderInfo.RESIZE_NONE); + info.widgetCategory = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_widgetCategory, + AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN); + + sa.recycle(); + } catch (Exception e) { + // Ok to catch Exception here, because anything going wrong because + // of what a client process passes to us should not be fatal for the + // system process. + Slog.w(TAG, "XML parsing failed for AppWidget provider '" + component + '\'', e); + return null; + } finally { + if (parser != null) + parser.close(); + } + return p; + } + + int getUidForPackage(String packageName) throws PackageManager.NameNotFoundException { + PackageInfo pkgInfo = null; + try { + pkgInfo = mPm.getPackageInfo(packageName, 0, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + } + if (pkgInfo == null || pkgInfo.applicationInfo == null) { + throw new PackageManager.NameNotFoundException(); + } + return pkgInfo.applicationInfo.uid; + } + + int enforceSystemOrCallingUid(String packageName) throws IllegalArgumentException { + int callingUid = Binder.getCallingUid(); + if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID || callingUid == 0) { + return callingUid; + } + return enforceCallingUid(packageName); + } + + int enforceCallingUid(String packageName) throws IllegalArgumentException { + int callingUid = Binder.getCallingUid(); + int packageUid; + try { + packageUid = getUidForPackage(packageName); + } catch (PackageManager.NameNotFoundException ex) { + throw new IllegalArgumentException("packageName and uid don't match packageName=" + + packageName); + } + if (!UserHandle.isSameApp(callingUid, packageUid)) { + throw new IllegalArgumentException("packageName and uid don't match packageName=" + + packageName); + } + return callingUid; + } + + void sendInitialBroadcasts() { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.instances.size() > 0) { + sendEnableIntentLocked(p); + int[] appWidgetIds = getAppWidgetIds(p); + sendUpdateIntentLocked(p, appWidgetIds); + registerForBroadcastsLocked(p, appWidgetIds); + } + } + } + } + + // only call from initialization -- it assumes that the data structures are all empty + void loadStateLocked() { + AtomicFile file = savedStateFile(); + try { + FileInputStream stream = file.openRead(); + readStateFromFileLocked(stream); + + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Slog.w(TAG, "Failed to close state FileInputStream " + e); + } + } + } catch (FileNotFoundException e) { + Slog.w(TAG, "Failed to read state: " + e); + } + } + + void saveStateLocked() { + if (!mHasFeature) { + return; + } + AtomicFile file = savedStateFile(); + FileOutputStream stream; + try { + stream = file.startWrite(); + if (writeStateToFileLocked(stream)) { + file.finishWrite(stream); + } else { + file.failWrite(stream); + Slog.w(TAG, "Failed to save state, restoring backup."); + } + } catch (IOException e) { + Slog.w(TAG, "Failed open state file for write: " + e); + } + } + + boolean writeStateToFileLocked(FileOutputStream stream) { + int N; + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, "utf-8"); + out.startDocument(null, true); + out.startTag(null, "gs"); + out.attribute(null, "version", String.valueOf(CURRENT_VERSION)); + int providerIndex = 0; + N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.instances.size() > 0) { + out.startTag(null, "p"); + out.attribute(null, "pkg", p.info.provider.getPackageName()); + out.attribute(null, "cl", p.info.provider.getClassName()); + out.endTag(null, "p"); + p.tag = providerIndex; + providerIndex++; + } + } + + N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host host = mHosts.get(i); + out.startTag(null, "h"); + out.attribute(null, "pkg", host.packageName); + out.attribute(null, "id", Integer.toHexString(host.hostId)); + out.endTag(null, "h"); + host.tag = i; + } + + N = mAppWidgetIds.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = mAppWidgetIds.get(i); + out.startTag(null, "g"); + out.attribute(null, "id", Integer.toHexString(id.appWidgetId)); + out.attribute(null, "h", Integer.toHexString(id.host.tag)); + if (id.provider != null) { + out.attribute(null, "p", Integer.toHexString(id.provider.tag)); + } + if (id.options != null) { + out.attribute(null, "min_width", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH))); + out.attribute(null, "min_height", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT))); + out.attribute(null, "max_width", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH))); + out.attribute(null, "max_height", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT))); + out.attribute(null, "host_category", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY))); + } + out.endTag(null, "g"); + } + + Iterator it = mPackagesWithBindWidgetPermission.iterator(); + while (it.hasNext()) { + out.startTag(null, "b"); + out.attribute(null, "packageName", it.next()); + out.endTag(null, "b"); + } + + out.endTag(null, "gs"); + + out.endDocument(); + return true; + } catch (IOException e) { + Slog.w(TAG, "Failed to write state: " + e); + return false; + } + } + + @SuppressWarnings("unused") + void readStateFromFileLocked(FileInputStream stream) { + boolean success = false; + int version = 0; + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, null); + + int type; + int providerIndex = 0; + HashMap loadedProviders = new HashMap(); + do { + type = parser.next(); + if (type == XmlPullParser.START_TAG) { + String tag = parser.getName(); + if ("gs".equals(tag)) { + String attributeValue = parser.getAttributeValue(null, "version"); + try { + version = Integer.parseInt(attributeValue); + } catch (NumberFormatException e) { + version = 0; + } + } else if ("p".equals(tag)) { + // TODO: do we need to check that this package has the same signature + // as before? + String pkg = parser.getAttributeValue(null, "pkg"); + String cl = parser.getAttributeValue(null, "cl"); + + final IPackageManager packageManager = AppGlobals.getPackageManager(); + try { + packageManager.getReceiverInfo(new ComponentName(pkg, cl), 0, mUserId); + } catch (RemoteException e) { + String[] pkgs = mContext.getPackageManager() + .currentToCanonicalPackageNames(new String[] { pkg }); + pkg = pkgs[0]; + } + + Provider p = lookupProviderLocked(new ComponentName(pkg, cl)); + if (p == null && mSafeMode) { + // if we're in safe mode, make a temporary one + p = new Provider(); + p.info = new AppWidgetProviderInfo(); + p.info.provider = new ComponentName(pkg, cl); + p.zombie = true; + mInstalledProviders.add(p); + } + if (p != null) { + // if it wasn't uninstalled or something + loadedProviders.put(providerIndex, p); + } + providerIndex++; + } else if ("h".equals(tag)) { + Host host = new Host(); + + // TODO: do we need to check that this package has the same signature + // as before? + host.packageName = parser.getAttributeValue(null, "pkg"); + try { + host.uid = getUidForPackage(host.packageName); + } catch (PackageManager.NameNotFoundException ex) { + host.zombie = true; + } + if (!host.zombie || mSafeMode) { + // In safe mode, we don't discard the hosts we don't recognize + // so that they're not pruned from our list. Otherwise, we do. + host.hostId = Integer + .parseInt(parser.getAttributeValue(null, "id"), 16); + mHosts.add(host); + } + } else if ("b".equals(tag)) { + String packageName = parser.getAttributeValue(null, "packageName"); + if (packageName != null) { + mPackagesWithBindWidgetPermission.add(packageName); + } + } else if ("g".equals(tag)) { + AppWidgetId id = new AppWidgetId(); + id.appWidgetId = Integer.parseInt(parser.getAttributeValue(null, "id"), 16); + if (id.appWidgetId >= mNextAppWidgetId) { + mNextAppWidgetId = id.appWidgetId + 1; + } + + Bundle options = new Bundle(); + String minWidthString = parser.getAttributeValue(null, "min_width"); + if (minWidthString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + Integer.parseInt(minWidthString, 16)); + } + String minHeightString = parser.getAttributeValue(null, "min_height"); + if (minHeightString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + Integer.parseInt(minHeightString, 16)); + } + String maxWidthString = parser.getAttributeValue(null, "max_width"); + if (maxWidthString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + Integer.parseInt(maxWidthString, 16)); + } + String maxHeightString = parser.getAttributeValue(null, "max_height"); + if (maxHeightString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + Integer.parseInt(maxHeightString, 16)); + } + String categoryString = parser.getAttributeValue(null, "host_category"); + if (categoryString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + Integer.parseInt(categoryString, 16)); + } + id.options = options; + + String providerString = parser.getAttributeValue(null, "p"); + if (providerString != null) { + // there's no provider if it hasn't been bound yet. + // maybe we don't have to save this, but it brings the system + // to the state it was in. + int pIndex = Integer.parseInt(providerString, 16); + id.provider = loadedProviders.get(pIndex); + if (false) { + Slog.d(TAG, "bound appWidgetId=" + id.appWidgetId + " to provider " + + pIndex + " which is " + id.provider); + } + if (id.provider == null) { + // This provider is gone. We just let the host figure out + // that this happened when it fails to load it. + continue; + } + } + + int hIndex = Integer.parseInt(parser.getAttributeValue(null, "h"), 16); + id.host = mHosts.get(hIndex); + if (id.host == null) { + // This host is gone. + continue; + } + + if (id.provider != null) { + id.provider.instances.add(id); + } + id.host.instances.add(id); + mAppWidgetIds.add(id); + } + } + } while (type != XmlPullParser.END_DOCUMENT); + success = true; + } catch (NullPointerException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (IOException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "failed parsing " + e); + } + + if (success) { + // delete any hosts that didn't manage to get connected (should happen) + // if it matters, they'll be reconnected. + for (int i = mHosts.size() - 1; i >= 0; i--) { + pruneHostLocked(mHosts.get(i)); + } + // upgrade the database if needed + performUpgrade(version); + } else { + // failed reading, clean up + Slog.w(TAG, "Failed to read state, clearing widgets and hosts."); + + mAppWidgetIds.clear(); + mHosts.clear(); + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + mInstalledProviders.get(i).instances.clear(); + } + } + } + + private void performUpgrade(int fromVersion) { + if (fromVersion < CURRENT_VERSION) { + Slog.v(TAG, "Upgrading widget database from " + fromVersion + " to " + CURRENT_VERSION + + " for user " + mUserId); + } + + int version = fromVersion; + + // Update 1: keyguard moved from package "android" to "com.android.keyguard" + if (version == 0) { + for (int i = 0; i < mHosts.size(); i++) { + Host host = mHosts.get(i); + if (host != null && "android".equals(host.packageName) + && host.hostId == KEYGUARD_HOST_ID) { + host.packageName = KEYGUARD_HOST_PACKAGE; + } + } + version = 1; + } + + if (version != CURRENT_VERSION) { + throw new IllegalStateException("Failed to upgrade widget database"); + } + } + + static File getSettingsFile(int userId) { + return new File(Environment.getUserSystemDirectory(userId), SETTINGS_FILENAME); + } + + AtomicFile savedStateFile() { + File dir = Environment.getUserSystemDirectory(mUserId); + File settingsFile = getSettingsFile(mUserId); + if (!settingsFile.exists() && mUserId == 0) { + if (!dir.exists()) { + dir.mkdirs(); + } + // Migrate old data + File oldFile = new File("/data/system/" + SETTINGS_FILENAME); + // Method doesn't throw an exception on failure. Ignore any errors + // in moving the file (like non-existence) + oldFile.renameTo(settingsFile); + } + return new AtomicFile(settingsFile); + } + + void onUserStopping() { + // prune the ones we don't want to keep + int N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + cancelBroadcasts(p); + } + } + + void onUserRemoved() { + getSettingsFile(mUserId).delete(); + } + + boolean addProvidersForPackageLocked(String pkgName) { + boolean providersAdded = false; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.setPackage(pkgName); + List broadcastReceivers; + try { + broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + return false; + } + final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + ActivityInfo ai = ri.activityInfo; + if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + continue; + } + if (pkgName.equals(ai.packageName)) { + addProviderLocked(ri); + providersAdded = true; + } + } + + return providersAdded; + } + + /** + * Updates all providers with the specified package names, and records any providers that were + * pruned. + * + * @return whether any providers were updated + */ + boolean updateProvidersForPackageLocked(String pkgName, Set removedProviders) { + boolean providersUpdated = false; + HashSet keep = new HashSet(); + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.setPackage(pkgName); + List broadcastReceivers; + try { + broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + return false; + } + + // add the missing ones and collect which ones to keep + int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + ActivityInfo ai = ri.activityInfo; + if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + continue; + } + if (pkgName.equals(ai.packageName)) { + ComponentName component = new ComponentName(ai.packageName, ai.name); + Provider p = lookupProviderLocked(component); + if (p == null) { + if (addProviderLocked(ri)) { + keep.add(ai.name); + providersUpdated = true; + } + } else { + Provider parsed = parseProviderInfoXml(component, ri); + if (parsed != null) { + keep.add(ai.name); + // Use the new AppWidgetProviderInfo. + p.info = parsed.info; + // If it's enabled + final int M = p.instances.size(); + if (M > 0) { + int[] appWidgetIds = getAppWidgetIds(p); + // Reschedule for the new updatePeriodMillis (don't worry about handling + // it specially if updatePeriodMillis didn't change because we just sent + // an update, and the next one will be updatePeriodMillis from now). + cancelBroadcasts(p); + registerForBroadcastsLocked(p, appWidgetIds); + // If it's currently showing, call back with the new + // AppWidgetProviderInfo. + for (int j = 0; j < M; j++) { + AppWidgetId id = p.instances.get(j); + id.views = null; + if (id.host != null && id.host.callbacks != null) { + try { + id.host.callbacks.providerChanged(id.appWidgetId, p.info, + mUserId); + } catch (RemoteException ex) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this + // instance. + id.host.callbacks = null; + } + } + } + // Now that we've told the host, push out an update. + sendUpdateIntentLocked(p, appWidgetIds); + providersUpdated = true; + } + } + } + } + } + + // prune the ones we don't want to keep + N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + if (pkgName.equals(p.info.provider.getPackageName()) + && !keep.contains(p.info.provider.getClassName())) { + if (removedProviders != null) { + removedProviders.add(p.info.provider); + } + removeProviderLocked(i, p); + providersUpdated = true; + } + } + + return providersUpdated; + } + + boolean removeProvidersForPackageLocked(String pkgName) { + boolean providersRemoved = false; + int N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + if (pkgName.equals(p.info.provider.getPackageName())) { + removeProviderLocked(i, p); + providersRemoved = true; + } + } + + // Delete the hosts for this package too + // + // By now, we have removed any AppWidgets that were in any hosts here, + // so we don't need to worry about sending DISABLE broadcasts to them. + N = mHosts.size(); + for (int i = N - 1; i >= 0; i--) { + Host host = mHosts.get(i); + if (pkgName.equals(host.packageName)) { + deleteHostLocked(host); + } + } + + return providersRemoved; + } + + void notifyHostsForProvidersChangedLocked() { + final int N = mHosts.size(); + for (int i = N - 1; i >= 0; i--) { + Host host = mHosts.get(i); + try { + if (host.callbacks != null) { + host.callbacks.providersChanged(mUserId); + } + } catch (RemoteException ex) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this + // instance. + host.callbacks = null; + } + } + } +} diff --git a/services/java/com/android/server/backup/BackupManagerService.java b/services/java/com/android/server/backup/BackupManagerService.java new file mode 100644 index 000000000000..c2b0d10cff49 --- /dev/null +++ b/services/java/com/android/server/backup/BackupManagerService.java @@ -0,0 +1,6196 @@ +/* + * 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 com.android.server.backup; + +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.IActivityManager; +import android.app.IApplicationThread; +import android.app.IBackupAgent; +import android.app.PendingIntent; +import android.app.backup.BackupAgent; +import android.app.backup.BackupDataOutput; +import android.app.backup.FullBackup; +import android.app.backup.RestoreSet; +import android.app.backup.IBackupManager; +import android.app.backup.IFullBackupRestoreObserver; +import android.app.backup.IRestoreObserver; +import android.app.backup.IRestoreSession; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +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.IPackageDeleteObserver; +import android.content.pm.IPackageInstallObserver; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.SELinux; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.WorkSource; +import android.os.Environment.UserEnvironment; +import android.os.storage.IMountService; +import android.provider.Settings; +import android.util.EventLog; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.StringBuilderPrinter; + +import com.android.internal.backup.BackupConstants; +import com.android.internal.backup.IBackupTransport; +import com.android.internal.backup.IObbBackupService; +import com.android.internal.backup.LocalTransport; +import com.android.server.EventLogTags; +import com.android.server.backup.PackageManagerBackupAgent.Metadata; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class BackupManagerService extends IBackupManager.Stub { + private static final String TAG = "BackupManagerService"; + private static final boolean DEBUG = true; + private static final boolean MORE_DEBUG = false; + + // Name and current contents version of the full-backup manifest file + static final String BACKUP_MANIFEST_FILENAME = "_manifest"; + static final int BACKUP_MANIFEST_VERSION = 1; + static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; + static final int BACKUP_FILE_VERSION = 1; + static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production + + static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; + static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; + + // How often we perform a backup pass. Privileged external callers can + // trigger an immediate pass. + private static final long BACKUP_INTERVAL = AlarmManager.INTERVAL_HOUR; + + // Random variation in backup scheduling time to avoid server load spikes + private static final int FUZZ_MILLIS = 5 * 60 * 1000; + + // The amount of time between the initial provisioning of the device and + // the first backup pass. + private static final long FIRST_BACKUP_INTERVAL = 12 * AlarmManager.INTERVAL_HOUR; + + // Retry interval for clear/init when the transport is unavailable + private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR; + + private static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN"; + private static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT"; + private static final String RUN_CLEAR_ACTION = "android.app.backup.intent.CLEAR"; + private static final int MSG_RUN_BACKUP = 1; + private static final int MSG_RUN_FULL_BACKUP = 2; + private static final int MSG_RUN_RESTORE = 3; + private static final int MSG_RUN_CLEAR = 4; + private static final int MSG_RUN_INITIALIZE = 5; + private static final int MSG_RUN_GET_RESTORE_SETS = 6; + private static final int MSG_TIMEOUT = 7; + private static final int MSG_RESTORE_TIMEOUT = 8; + private static final int MSG_FULL_CONFIRMATION_TIMEOUT = 9; + private static final int MSG_RUN_FULL_RESTORE = 10; + private static final int MSG_RETRY_INIT = 11; + private static final int MSG_RETRY_CLEAR = 12; + + // backup task state machine tick + static final int MSG_BACKUP_RESTORE_STEP = 20; + static final int MSG_OP_COMPLETE = 21; + + // Timeout interval for deciding that a bind or clear-data has taken too long + static final long TIMEOUT_INTERVAL = 10 * 1000; + + // Timeout intervals for agent backup & restore operations + static final long TIMEOUT_BACKUP_INTERVAL = 30 * 1000; + static final long TIMEOUT_FULL_BACKUP_INTERVAL = 5 * 60 * 1000; + static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000; + static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000; + + // User confirmation timeout for a full backup/restore operation. It's this long in + // order to give them time to enter the backup password. + static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000; + + private Context mContext; + private PackageManager mPackageManager; + IPackageManager mPackageManagerBinder; + private IActivityManager mActivityManager; + private PowerManager mPowerManager; + private AlarmManager mAlarmManager; + private IMountService mMountService; + IBackupManager mBackupManagerBinder; + + boolean mEnabled; // access to this is synchronized on 'this' + boolean mProvisioned; + boolean mAutoRestore; + PowerManager.WakeLock mWakelock; + HandlerThread mHandlerThread; + BackupHandler mBackupHandler; + PendingIntent mRunBackupIntent, mRunInitIntent; + BroadcastReceiver mRunBackupReceiver, mRunInitReceiver; + // map UIDs to the set of participating packages under that UID + final SparseArray> mBackupParticipants + = new SparseArray>(); + // set of backup services that have pending changes + class BackupRequest { + public String packageName; + + BackupRequest(String pkgName) { + packageName = pkgName; + } + + public String toString() { + return "BackupRequest{pkg=" + packageName + "}"; + } + } + // Backups that we haven't started yet. Keys are package names. + HashMap mPendingBackups + = new HashMap(); + + // Pseudoname that we use for the Package Manager metadata "package" + static final String PACKAGE_MANAGER_SENTINEL = "@pm@"; + + // locking around the pending-backup management + final Object mQueueLock = new Object(); + + // The thread performing the sequence of queued backups binds to each app's agent + // in succession. Bind notifications are asynchronously delivered through the + // Activity Manager; use this lock object to signal when a requested binding has + // completed. + final Object mAgentConnectLock = new Object(); + IBackupAgent mConnectedAgent; + volatile boolean mBackupRunning; + volatile boolean mConnecting; + volatile long mLastBackupPass; + volatile long mNextBackupPass; + + // For debugging, we maintain a progress trace of operations during backup + static final boolean DEBUG_BACKUP_TRACE = true; + final List mBackupTrace = new ArrayList(); + + // A similar synchronization mechanism around clearing apps' data for restore + final Object mClearDataLock = new Object(); + volatile boolean mClearingData; + + // Transport bookkeeping + final HashMap mTransportNames + = new HashMap(); // component name -> registration name + final HashMap mTransports + = new HashMap(); // registration name -> binder + final ArrayList mTransportConnections + = new ArrayList(); + String mCurrentTransport; + ActiveRestoreSession mActiveRestoreSession; + + // Watch the device provisioning operation during setup + ContentObserver mProvisionedObserver; + + class ProvisionedObserver extends ContentObserver { + public ProvisionedObserver(Handler handler) { + super(handler); + } + + public void onChange(boolean selfChange) { + final boolean wasProvisioned = mProvisioned; + final boolean isProvisioned = deviceIsProvisioned(); + // latch: never unprovision + mProvisioned = wasProvisioned || isProvisioned; + if (MORE_DEBUG) { + Slog.d(TAG, "Provisioning change: was=" + wasProvisioned + + " is=" + isProvisioned + " now=" + mProvisioned); + } + + synchronized (mQueueLock) { + if (mProvisioned && !wasProvisioned && mEnabled) { + // we're now good to go, so start the backup alarms + if (MORE_DEBUG) Slog.d(TAG, "Now provisioned, so starting backups"); + startBackupAlarmsLocked(FIRST_BACKUP_INTERVAL); + } + } + } + } + + class RestoreGetSetsParams { + public IBackupTransport transport; + public ActiveRestoreSession session; + public IRestoreObserver observer; + + RestoreGetSetsParams(IBackupTransport _transport, ActiveRestoreSession _session, + IRestoreObserver _observer) { + transport = _transport; + session = _session; + observer = _observer; + } + } + + class RestoreParams { + public IBackupTransport transport; + public String dirName; + public IRestoreObserver observer; + public long token; + public PackageInfo pkgInfo; + public int pmToken; // in post-install restore, the PM's token for this transaction + public boolean needFullBackup; + public String[] filterSet; + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, PackageInfo _pkg, int _pmToken, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = _pkg; + pmToken = _pmToken; + needFullBackup = _needFullBackup; + filterSet = null; + } + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = null; + pmToken = 0; + needFullBackup = _needFullBackup; + filterSet = null; + } + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, String[] _filterSet, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = null; + pmToken = 0; + needFullBackup = _needFullBackup; + filterSet = _filterSet; + } + } + + class ClearParams { + public IBackupTransport transport; + public PackageInfo packageInfo; + + ClearParams(IBackupTransport _transport, PackageInfo _info) { + transport = _transport; + packageInfo = _info; + } + } + + class ClearRetryParams { + public String transportName; + public String packageName; + + ClearRetryParams(String transport, String pkg) { + transportName = transport; + packageName = pkg; + } + } + + class FullParams { + public ParcelFileDescriptor fd; + public final AtomicBoolean latch; + public IFullBackupRestoreObserver observer; + public String curPassword; // filled in by the confirmation step + public String encryptPassword; + + FullParams() { + latch = new AtomicBoolean(false); + } + } + + class FullBackupParams extends FullParams { + public boolean includeApks; + public boolean includeObbs; + public boolean includeShared; + public boolean allApps; + public boolean includeSystem; + public String[] packages; + + FullBackupParams(ParcelFileDescriptor output, boolean saveApks, boolean saveObbs, + boolean saveShared, boolean doAllApps, boolean doSystem, String[] pkgList) { + fd = output; + includeApks = saveApks; + includeObbs = saveObbs; + includeShared = saveShared; + allApps = doAllApps; + includeSystem = doSystem; + packages = pkgList; + } + } + + class FullRestoreParams extends FullParams { + FullRestoreParams(ParcelFileDescriptor input) { + fd = input; + } + } + + // Bookkeeping of in-flight operations for timeout etc. purposes. The operation + // token is the index of the entry in the pending-operations list. + static final int OP_PENDING = 0; + static final int OP_ACKNOWLEDGED = 1; + static final int OP_TIMEOUT = -1; + + class Operation { + public int state; + public BackupRestoreTask callback; + + Operation(int initialState, BackupRestoreTask callbackObj) { + state = initialState; + callback = callbackObj; + } + } + final SparseArray mCurrentOperations = new SparseArray(); + final Object mCurrentOpLock = new Object(); + final Random mTokenGenerator = new Random(); + + final SparseArray mFullConfirmations = new SparseArray(); + + // Where we keep our journal files and other bookkeeping + File mBaseStateDir; + File mDataDir; + File mJournalDir; + File mJournal; + + // Backup password, if any, and the file where it's saved. What is stored is not the + // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but + // persisted) salt. Validation is performed by running the challenge text through the + // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches + // the saved hash string, then the challenge text matches the originally supplied + // password text. + private final SecureRandom mRng = new SecureRandom(); + private String mPasswordHash; + private File mPasswordHashFile; + private byte[] mPasswordSalt; + + // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys + static final int PBKDF2_HASH_ROUNDS = 10000; + static final int PBKDF2_KEY_SIZE = 256; // bits + static final int PBKDF2_SALT_SIZE = 512; // bits + static final String ENCRYPTION_ALGORITHM_NAME = "AES-256"; + + // Keep a log of all the apps we've ever backed up, and what the + // dataset tokens are for both the current backup dataset and + // the ancestral dataset. + private File mEverStored; + HashSet mEverStoredApps = new HashSet(); + + static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1; // increment when the schema changes + File mTokenFile; + Set mAncestralPackages = null; + long mAncestralToken = 0; + long mCurrentToken = 0; + + // Persistently track the need to do a full init + static final String INIT_SENTINEL_FILE_NAME = "_need_init_"; + HashSet mPendingInits = new HashSet(); // transport names + + // Utility: build a new random integer token + int generateToken() { + int token; + do { + synchronized (mTokenGenerator) { + token = mTokenGenerator.nextInt(); + } + } while (token < 0); + return token; + } + + // ----- Asynchronous backup/restore handler thread ----- + + private class BackupHandler extends Handler { + public BackupHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + + switch (msg.what) { + case MSG_RUN_BACKUP: + { + mLastBackupPass = System.currentTimeMillis(); + mNextBackupPass = mLastBackupPass + BACKUP_INTERVAL; + + IBackupTransport transport = getTransport(mCurrentTransport); + if (transport == null) { + Slog.v(TAG, "Backup requested but no transport available"); + synchronized (mQueueLock) { + mBackupRunning = false; + } + mWakelock.release(); + break; + } + + // snapshot the pending-backup set and work on that + ArrayList queue = new ArrayList(); + File oldJournal = mJournal; + synchronized (mQueueLock) { + // Do we have any work to do? Construct the work queue + // then release the synchronization lock to actually run + // the backup. + if (mPendingBackups.size() > 0) { + for (BackupRequest b: mPendingBackups.values()) { + queue.add(b); + } + if (DEBUG) Slog.v(TAG, "clearing pending backups"); + mPendingBackups.clear(); + + // Start a new backup-queue journal file too + mJournal = null; + + } + } + + // At this point, we have started a new journal file, and the old + // file identity is being passed to the backup processing task. + // When it completes successfully, that old journal file will be + // deleted. If we crash prior to that, the old journal is parsed + // at next boot and the journaled requests fulfilled. + boolean staged = true; + if (queue.size() > 0) { + // Spin up a backup state sequence and set it running + try { + String dirName = transport.transportDirName(); + PerformBackupTask pbt = new PerformBackupTask(transport, dirName, + queue, oldJournal); + Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt); + sendMessage(pbtMessage); + } catch (RemoteException e) { + // unable to ask the transport its dir name -- transient failure, since + // the above check succeeded. Try again next time. + Slog.e(TAG, "Transport became unavailable attempting backup"); + staged = false; + } + } else { + Slog.v(TAG, "Backup requested but nothing pending"); + staged = false; + } + + if (!staged) { + // if we didn't actually hand off the wakelock, rewind until next time + synchronized (mQueueLock) { + mBackupRunning = false; + } + mWakelock.release(); + } + break; + } + + case MSG_BACKUP_RESTORE_STEP: + { + try { + BackupRestoreTask task = (BackupRestoreTask) msg.obj; + if (MORE_DEBUG) Slog.v(TAG, "Got next step for " + task + ", executing"); + task.execute(); + } catch (ClassCastException e) { + Slog.e(TAG, "Invalid backup task in flight, obj=" + msg.obj); + } + break; + } + + case MSG_OP_COMPLETE: + { + try { + BackupRestoreTask task = (BackupRestoreTask) msg.obj; + task.operationComplete(); + } catch (ClassCastException e) { + Slog.e(TAG, "Invalid completion in flight, obj=" + msg.obj); + } + break; + } + + case MSG_RUN_FULL_BACKUP: + { + // TODO: refactor full backup to be a looper-based state machine + // similar to normal backup/restore. + FullBackupParams params = (FullBackupParams)msg.obj; + PerformFullBackupTask task = new PerformFullBackupTask(params.fd, + params.observer, params.includeApks, params.includeObbs, + params.includeShared, params.curPassword, params.encryptPassword, + params.allApps, params.includeSystem, params.packages, params.latch); + (new Thread(task)).start(); + break; + } + + case MSG_RUN_RESTORE: + { + RestoreParams params = (RestoreParams)msg.obj; + Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer); + PerformRestoreTask task = new PerformRestoreTask( + params.transport, params.dirName, params.observer, + params.token, params.pkgInfo, params.pmToken, + params.needFullBackup, params.filterSet); + Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task); + sendMessage(restoreMsg); + break; + } + + case MSG_RUN_FULL_RESTORE: + { + // TODO: refactor full restore to be a looper-based state machine + // similar to normal backup/restore. + FullRestoreParams params = (FullRestoreParams)msg.obj; + PerformFullRestoreTask task = new PerformFullRestoreTask(params.fd, + params.curPassword, params.encryptPassword, + params.observer, params.latch); + (new Thread(task)).start(); + break; + } + + case MSG_RUN_CLEAR: + { + ClearParams params = (ClearParams)msg.obj; + (new PerformClearTask(params.transport, params.packageInfo)).run(); + break; + } + + case MSG_RETRY_CLEAR: + { + // reenqueues if the transport remains unavailable + ClearRetryParams params = (ClearRetryParams)msg.obj; + clearBackupData(params.transportName, params.packageName); + break; + } + + case MSG_RUN_INITIALIZE: + { + HashSet queue; + + // Snapshot the pending-init queue and work on that + synchronized (mQueueLock) { + queue = new HashSet(mPendingInits); + mPendingInits.clear(); + } + + (new PerformInitializeTask(queue)).run(); + break; + } + + case MSG_RETRY_INIT: + { + synchronized (mQueueLock) { + recordInitPendingLocked(msg.arg1 != 0, (String)msg.obj); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), + mRunInitIntent); + } + break; + } + + case MSG_RUN_GET_RESTORE_SETS: + { + // Like other async operations, this is entered with the wakelock held + RestoreSet[] sets = null; + RestoreGetSetsParams params = (RestoreGetSetsParams)msg.obj; + try { + sets = params.transport.getAvailableRestoreSets(); + // cache the result in the active session + synchronized (params.session) { + params.session.mRestoreSets = sets; + } + if (sets == null) EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + } catch (Exception e) { + Slog.e(TAG, "Error from transport getting set list"); + } finally { + if (params.observer != null) { + try { + params.observer.restoreSetsAvailable(sets); + } catch (RemoteException re) { + Slog.e(TAG, "Unable to report listing to observer"); + } catch (Exception e) { + Slog.e(TAG, "Restore observer threw", e); + } + } + + // Done: reset the session timeout clock + removeMessages(MSG_RESTORE_TIMEOUT); + sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); + + mWakelock.release(); + } + break; + } + + case MSG_TIMEOUT: + { + handleTimeout(msg.arg1, msg.obj); + break; + } + + case MSG_RESTORE_TIMEOUT: + { + synchronized (BackupManagerService.this) { + if (mActiveRestoreSession != null) { + // Client app left the restore session dangling. We know that it + // can't be in the middle of an actual restore operation because + // the timeout is suspended while a restore is in progress. Clean + // up now. + Slog.w(TAG, "Restore session timed out; aborting"); + post(mActiveRestoreSession.new EndRestoreRunnable( + BackupManagerService.this, mActiveRestoreSession)); + } + } + } + + case MSG_FULL_CONFIRMATION_TIMEOUT: + { + synchronized (mFullConfirmations) { + FullParams params = mFullConfirmations.get(msg.arg1); + if (params != null) { + Slog.i(TAG, "Full backup/restore timed out waiting for user confirmation"); + + // Release the waiter; timeout == completion + signalFullBackupRestoreCompletion(params); + + // Remove the token from the set + mFullConfirmations.delete(msg.arg1); + + // Report a timeout to the observer, if any + if (params.observer != null) { + try { + params.observer.onTimeout(); + } catch (RemoteException e) { + /* don't care if the app has gone away */ + } + } + } else { + Slog.d(TAG, "couldn't find params for token " + msg.arg1); + } + } + break; + } + } + } + } + + // ----- Debug-only backup operation trace ----- + void addBackupTrace(String s) { + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + mBackupTrace.add(s); + } + } + } + + void clearBackupTrace() { + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + mBackupTrace.clear(); + } + } + } + + // ----- Main service implementation ----- + + public BackupManagerService(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + mPackageManagerBinder = AppGlobals.getPackageManager(); + mActivityManager = ActivityManagerNative.getDefault(); + + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + + mBackupManagerBinder = asInterface(asBinder()); + + // spin up the backup/restore handler thread + mHandlerThread = new HandlerThread("backup", Process.THREAD_PRIORITY_BACKGROUND); + mHandlerThread.start(); + mBackupHandler = new BackupHandler(mHandlerThread.getLooper()); + + // Set up our bookkeeping + final ContentResolver resolver = context.getContentResolver(); + boolean areEnabled = Settings.Secure.getInt(resolver, + Settings.Secure.BACKUP_ENABLED, 0) != 0; + mProvisioned = Settings.Global.getInt(resolver, + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + mAutoRestore = Settings.Secure.getInt(resolver, + Settings.Secure.BACKUP_AUTO_RESTORE, 1) != 0; + + mProvisionedObserver = new ProvisionedObserver(mBackupHandler); + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mProvisionedObserver); + + // If Encrypted file systems is enabled or disabled, this call will return the + // correct directory. + mBaseStateDir = new File(Environment.getSecureDataDirectory(), "backup"); + mBaseStateDir.mkdirs(); + if (!SELinux.restorecon(mBaseStateDir)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir); + } + mDataDir = Environment.getDownloadCacheDirectory(); + + mPasswordHashFile = new File(mBaseStateDir, "pwhash"); + if (mPasswordHashFile.exists()) { + FileInputStream fin = null; + DataInputStream in = null; + try { + fin = new FileInputStream(mPasswordHashFile); + in = new DataInputStream(new BufferedInputStream(fin)); + // integer length of the salt array, followed by the salt, + // then the hex pw hash string + int saltLen = in.readInt(); + byte[] salt = new byte[saltLen]; + in.readFully(salt); + mPasswordHash = in.readUTF(); + mPasswordSalt = salt; + } catch (IOException e) { + Slog.e(TAG, "Unable to read saved backup pw hash"); + } finally { + try { + if (in != null) in.close(); + if (fin != null) fin.close(); + } catch (IOException e) { + Slog.w(TAG, "Unable to close streams"); + } + } + } + + // Alarm receivers for scheduled backups & initialization operations + mRunBackupReceiver = new RunBackupReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(RUN_BACKUP_ACTION); + context.registerReceiver(mRunBackupReceiver, filter, + android.Manifest.permission.BACKUP, null); + + mRunInitReceiver = new RunInitializeReceiver(); + filter = new IntentFilter(); + filter.addAction(RUN_INITIALIZE_ACTION); + context.registerReceiver(mRunInitReceiver, filter, + android.Manifest.permission.BACKUP, null); + + Intent backupIntent = new Intent(RUN_BACKUP_ACTION); + backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mRunBackupIntent = PendingIntent.getBroadcast(context, MSG_RUN_BACKUP, backupIntent, 0); + + Intent initIntent = new Intent(RUN_INITIALIZE_ACTION); + backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mRunInitIntent = PendingIntent.getBroadcast(context, MSG_RUN_INITIALIZE, initIntent, 0); + + // Set up the backup-request journaling + mJournalDir = new File(mBaseStateDir, "pending"); + mJournalDir.mkdirs(); // creates mBaseStateDir along the way + mJournal = null; // will be created on first use + + // Set up the various sorts of package tracking we do + initPackageTracking(); + + // Build our mapping of uid to backup client services. This implicitly + // schedules a backup pass on the Package Manager metadata the first + // time anything needs to be backed up. + synchronized (mBackupParticipants) { + addPackageParticipantsLocked(null); + } + + // Set up our transport options and initialize the default transport + // TODO: Don't create transports that we don't need to? + mCurrentTransport = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.BACKUP_TRANSPORT); + if ("".equals(mCurrentTransport)) { + mCurrentTransport = null; + } + if (DEBUG) Slog.v(TAG, "Starting with transport " + mCurrentTransport); + + // Find transport hosts and bind to their services + Intent transportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST); + List hosts = mPackageManager.queryIntentServicesAsUser( + transportServiceIntent, 0, UserHandle.USER_OWNER); + if (DEBUG) { + Slog.v(TAG, "Found transports: " + ((hosts == null) ? "null" : hosts.size())); + } + if (hosts != null) { + if (MORE_DEBUG) { + for (int i = 0; i < hosts.size(); i++) { + ServiceInfo info = hosts.get(i).serviceInfo; + Slog.v(TAG, " " + info.packageName + "/" + info.name); + } + } + for (int i = 0; i < hosts.size(); i++) { + try { + ServiceInfo info = hosts.get(i).serviceInfo; + PackageInfo packInfo = mPackageManager.getPackageInfo(info.packageName, 0); + if ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0) { + ComponentName svcName = new ComponentName(info.packageName, info.name); + if (DEBUG) { + Slog.i(TAG, "Binding to transport host " + svcName); + } + Intent intent = new Intent(transportServiceIntent); + intent.setComponent(svcName); + TransportConnection connection = new TransportConnection(); + mTransportConnections.add(connection); + context.bindServiceAsUser(intent, + connection, Context.BIND_AUTO_CREATE, + UserHandle.OWNER); + } else { + Slog.w(TAG, "Transport package not privileged: " + info.packageName); + } + } catch (Exception e) { + Slog.e(TAG, "Problem resolving transport service: " + e.getMessage()); + } + } + } + + // Now that we know about valid backup participants, parse any + // leftover journal files into the pending backup set + parseLeftoverJournals(); + + // Power management + mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"); + + // Start the backup passes going + setBackupEnabled(areEnabled); + } + + private class RunBackupReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (RUN_BACKUP_ACTION.equals(intent.getAction())) { + synchronized (mQueueLock) { + if (mPendingInits.size() > 0) { + // If there are pending init operations, we process those + // and then settle into the usual periodic backup schedule. + if (DEBUG) Slog.v(TAG, "Init pending at scheduled backup"); + try { + mAlarmManager.cancel(mRunInitIntent); + mRunInitIntent.send(); + } catch (PendingIntent.CanceledException ce) { + Slog.e(TAG, "Run init intent cancelled"); + // can't really do more than bail here + } + } else { + // Don't run backups now if we're disabled or not yet + // fully set up. + if (mEnabled && mProvisioned) { + if (!mBackupRunning) { + if (DEBUG) Slog.v(TAG, "Running a backup pass"); + + // Acquire the wakelock and pass it to the backup thread. it will + // be released once backup concludes. + mBackupRunning = true; + mWakelock.acquire(); + + Message msg = mBackupHandler.obtainMessage(MSG_RUN_BACKUP); + mBackupHandler.sendMessage(msg); + } else { + Slog.i(TAG, "Backup time but one already running"); + } + } else { + Slog.w(TAG, "Backup pass but e=" + mEnabled + " p=" + mProvisioned); + } + } + } + } + } + } + + private class RunInitializeReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (RUN_INITIALIZE_ACTION.equals(intent.getAction())) { + synchronized (mQueueLock) { + if (DEBUG) Slog.v(TAG, "Running a device init"); + + // Acquire the wakelock and pass it to the init thread. it will + // be released once init concludes. + mWakelock.acquire(); + + Message msg = mBackupHandler.obtainMessage(MSG_RUN_INITIALIZE); + mBackupHandler.sendMessage(msg); + } + } + } + } + + private void initPackageTracking() { + if (DEBUG) Slog.v(TAG, "Initializing package tracking"); + + // Remember our ancestral dataset + mTokenFile = new File(mBaseStateDir, "ancestral"); + try { + RandomAccessFile tf = new RandomAccessFile(mTokenFile, "r"); + int version = tf.readInt(); + if (version == CURRENT_ANCESTRAL_RECORD_VERSION) { + mAncestralToken = tf.readLong(); + mCurrentToken = tf.readLong(); + + int numPackages = tf.readInt(); + if (numPackages >= 0) { + mAncestralPackages = new HashSet(); + for (int i = 0; i < numPackages; i++) { + String pkgName = tf.readUTF(); + mAncestralPackages.add(pkgName); + } + } + } + tf.close(); + } catch (FileNotFoundException fnf) { + // Probably innocuous + Slog.v(TAG, "No ancestral data"); + } catch (IOException e) { + Slog.w(TAG, "Unable to read token file", e); + } + + // Keep a log of what apps we've ever backed up. Because we might have + // rebooted in the middle of an operation that was removing something from + // this log, we sanity-check its contents here and reconstruct it. + mEverStored = new File(mBaseStateDir, "processed"); + File tempProcessedFile = new File(mBaseStateDir, "processed.new"); + + // If we were in the middle of removing something from the ever-backed-up + // file, there might be a transient "processed.new" file still present. + // Ignore it -- we'll validate "processed" against the current package set. + if (tempProcessedFile.exists()) { + tempProcessedFile.delete(); + } + + // If there are previous contents, parse them out then start a new + // file to continue the recordkeeping. + if (mEverStored.exists()) { + RandomAccessFile temp = null; + RandomAccessFile in = null; + + try { + temp = new RandomAccessFile(tempProcessedFile, "rws"); + in = new RandomAccessFile(mEverStored, "r"); + + while (true) { + PackageInfo info; + String pkg = in.readUTF(); + try { + info = mPackageManager.getPackageInfo(pkg, 0); + mEverStoredApps.add(pkg); + temp.writeUTF(pkg); + if (MORE_DEBUG) Slog.v(TAG, " + " + pkg); + } catch (NameNotFoundException e) { + // nope, this package was uninstalled; don't include it + if (MORE_DEBUG) Slog.v(TAG, " - " + pkg); + } + } + } catch (EOFException e) { + // Once we've rewritten the backup history log, atomically replace the + // old one with the new one then reopen the file for continuing use. + if (!tempProcessedFile.renameTo(mEverStored)) { + Slog.e(TAG, "Error renaming " + tempProcessedFile + " to " + mEverStored); + } + } catch (IOException e) { + Slog.e(TAG, "Error in processed file", e); + } finally { + try { if (temp != null) temp.close(); } catch (IOException e) {} + try { if (in != null) in.close(); } catch (IOException e) {} + } + } + + // Register for broadcasts about package install, etc., so we can + // update the provider list. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiver(mBroadcastReceiver, filter); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mBroadcastReceiver, sdFilter); + } + + private void parseLeftoverJournals() { + for (File f : mJournalDir.listFiles()) { + if (mJournal == null || f.compareTo(mJournal) != 0) { + // This isn't the current journal, so it must be a leftover. Read + // out the package names mentioned there and schedule them for + // backup. + RandomAccessFile in = null; + try { + Slog.i(TAG, "Found stale backup journal, scheduling"); + in = new RandomAccessFile(f, "r"); + while (true) { + String packageName = in.readUTF(); + Slog.i(TAG, " " + packageName); + dataChangedImpl(packageName); + } + } catch (EOFException e) { + // no more data; we're done + } catch (Exception e) { + Slog.e(TAG, "Can't read " + f, e); + } finally { + // close/delete the file + try { if (in != null) in.close(); } catch (IOException e) {} + f.delete(); + } + } + } + } + + private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) { + return buildCharArrayKey(pw.toCharArray(), salt, rounds); + } + + private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); + return keyFactory.generateSecret(ks); + } catch (InvalidKeySpecException e) { + Slog.e(TAG, "Invalid key spec for PBKDF2!"); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "PBKDF2 unavailable!"); + } + return null; + } + + private String buildPasswordHash(String pw, byte[] salt, int rounds) { + SecretKey key = buildPasswordKey(pw, salt, rounds); + if (key != null) { + return byteArrayToHex(key.getEncoded()); + } + return null; + } + + private String byteArrayToHex(byte[] data) { + StringBuilder buf = new StringBuilder(data.length * 2); + for (int i = 0; i < data.length; i++) { + buf.append(Byte.toHexString(data[i], true)); + } + return buf.toString(); + } + + private byte[] hexToByteArray(String digits) { + final int bytes = digits.length() / 2; + if (2*bytes != digits.length()) { + throw new IllegalArgumentException("Hex string must have an even number of digits"); + } + + byte[] result = new byte[bytes]; + for (int i = 0; i < digits.length(); i += 2) { + result[i/2] = (byte) Integer.parseInt(digits.substring(i, i+2), 16); + } + return result; + } + + private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { + char[] mkAsChar = new char[pwBytes.length]; + for (int i = 0; i < pwBytes.length; i++) { + mkAsChar[i] = (char) pwBytes[i]; + } + + Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); + return checksum.getEncoded(); + } + + // Used for generating random salts or passwords + private byte[] randomBytes(int bits) { + byte[] array = new byte[bits / 8]; + mRng.nextBytes(array); + return array; + } + + // Backup password management + boolean passwordMatchesSaved(String candidatePw, int rounds) { + // First, on an encrypted device we require matching the device pw + final boolean isEncrypted; + try { + isEncrypted = (mMountService.getEncryptionState() != + IMountService.ENCRYPTION_STATE_NONE); + if (isEncrypted) { + if (DEBUG) { + Slog.i(TAG, "Device encrypted; verifying against device data pw"); + } + // 0 means the password validated + // -2 means device not encrypted + // Any other result is either password failure or an error condition, + // so we refuse the match + final int result = mMountService.verifyEncryptionPassword(candidatePw); + if (result == 0) { + if (MORE_DEBUG) Slog.d(TAG, "Pw verifies"); + return true; + } else if (result != -2) { + if (MORE_DEBUG) Slog.d(TAG, "Pw mismatch"); + return false; + } else { + // ...else the device is supposedly not encrypted. HOWEVER, the + // query about the encryption state said that the device *is* + // encrypted, so ... we may have a problem. Log it and refuse + // the backup. + Slog.e(TAG, "verified encryption state mismatch against query; no match allowed"); + return false; + } + } + } catch (Exception e) { + // Something went wrong talking to the mount service. This is very bad; + // assume that we fail password validation. + return false; + } + + if (mPasswordHash == null) { + // no current password case -- require that 'currentPw' be null or empty + if (candidatePw == null || "".equals(candidatePw)) { + return true; + } // else the non-empty candidate does not match the empty stored pw + } else { + // hash the stated current pw and compare to the stored one + if (candidatePw != null && candidatePw.length() > 0) { + String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds); + if (mPasswordHash.equalsIgnoreCase(currentPwHash)) { + // candidate hash matches the stored hash -- the password matches + return true; + } + } // else the stored pw is nonempty but the candidate is empty; no match + } + return false; + } + + @Override + public boolean setBackupPassword(String currentPw, String newPw) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupPassword"); + + // If the supplied pw doesn't hash to the the saved one, fail + if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) { + return false; + } + + // Clearing the password is okay + if (newPw == null || newPw.isEmpty()) { + if (mPasswordHashFile.exists()) { + if (!mPasswordHashFile.delete()) { + // Unable to delete the old pw file, so fail + Slog.e(TAG, "Unable to clear backup password"); + return false; + } + } + mPasswordHash = null; + mPasswordSalt = null; + return true; + } + + try { + // Okay, build the hash of the new backup password + byte[] salt = randomBytes(PBKDF2_SALT_SIZE); + String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS); + + OutputStream pwf = null, buffer = null; + DataOutputStream out = null; + try { + pwf = new FileOutputStream(mPasswordHashFile); + buffer = new BufferedOutputStream(pwf); + out = new DataOutputStream(buffer); + // integer length of the salt array, followed by the salt, + // then the hex pw hash string + out.writeInt(salt.length); + out.write(salt); + out.writeUTF(newPwHash); + out.flush(); + mPasswordHash = newPwHash; + mPasswordSalt = salt; + return true; + } finally { + if (out != null) out.close(); + if (buffer != null) buffer.close(); + if (pwf != null) pwf.close(); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to set backup password"); + } + return false; + } + + @Override + public boolean hasBackupPassword() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "hasBackupPassword"); + + try { + return (mMountService.getEncryptionState() != IMountService.ENCRYPTION_STATE_NONE) + || (mPasswordHash != null && mPasswordHash.length() > 0); + } catch (Exception e) { + // If we can't talk to the mount service we have a serious problem; fail + // "secure" i.e. assuming that we require a password + return true; + } + } + + // Maintain persistent state around whether need to do an initialize operation. + // Must be called with the queue lock held. + void recordInitPendingLocked(boolean isPending, String transportName) { + if (DEBUG) Slog.i(TAG, "recordInitPendingLocked: " + isPending + + " on transport " + transportName); + mBackupHandler.removeMessages(MSG_RETRY_INIT); + + try { + IBackupTransport transport = getTransport(transportName); + if (transport != null) { + String transportDirName = transport.transportDirName(); + File stateDir = new File(mBaseStateDir, transportDirName); + File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME); + + if (isPending) { + // We need an init before we can proceed with sending backup data. + // Record that with an entry in our set of pending inits, as well as + // journaling it via creation of a sentinel file. + mPendingInits.add(transportName); + try { + (new FileOutputStream(initPendingFile)).close(); + } catch (IOException ioe) { + // Something is badly wrong with our permissions; just try to move on + } + } else { + // No more initialization needed; wipe the journal and reset our state. + initPendingFile.delete(); + mPendingInits.remove(transportName); + } + return; // done; don't fall through to the error case + } + } catch (RemoteException e) { + // transport threw when asked its name; fall through to the lookup-failed case + } + + // The named transport doesn't exist or threw. This operation is + // important, so we record the need for a an init and post a message + // to retry the init later. + if (isPending) { + mPendingInits.add(transportName); + mBackupHandler.sendMessageDelayed( + mBackupHandler.obtainMessage(MSG_RETRY_INIT, + (isPending ? 1 : 0), + 0, + transportName), + TRANSPORT_RETRY_INTERVAL); + } + } + + // Reset all of our bookkeeping, in response to having been told that + // the backend data has been wiped [due to idle expiry, for example], + // so we must re-upload all saved settings. + void resetBackupState(File stateFileDir) { + synchronized (mQueueLock) { + // Wipe the "what we've ever backed up" tracking + mEverStoredApps.clear(); + mEverStored.delete(); + + mCurrentToken = 0; + writeRestoreTokens(); + + // Remove all the state files + for (File sf : stateFileDir.listFiles()) { + // ... but don't touch the needs-init sentinel + if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) { + sf.delete(); + } + } + } + + // Enqueue a new backup of every participant + synchronized (mBackupParticipants) { + final int N = mBackupParticipants.size(); + for (int i=0; i participants = mBackupParticipants.valueAt(i); + if (participants != null) { + for (String packageName : participants) { + dataChangedImpl(packageName); + } + } + } + } + } + + // Add a transport to our set of available backends. If 'transport' is null, this + // is an unregistration, and the transport's entry is removed from our bookkeeping. + private void registerTransport(String name, String component, IBackupTransport transport) { + synchronized (mTransports) { + if (DEBUG) Slog.v(TAG, "Registering transport " + + component + "::" + name + " = " + transport); + if (transport != null) { + mTransports.put(name, transport); + mTransportNames.put(component, name); + } else { + mTransports.remove(mTransportNames.get(component)); + mTransportNames.remove(component); + // Nothing further to do in the unregistration case + return; + } + } + + // If the init sentinel file exists, we need to be sure to perform the init + // as soon as practical. We also create the state directory at registration + // time to ensure it's present from the outset. + try { + String transportName = transport.transportDirName(); + File stateDir = new File(mBaseStateDir, transportName); + stateDir.mkdirs(); + + File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME); + if (initSentinel.exists()) { + synchronized (mQueueLock) { + mPendingInits.add(transportName); + + // TODO: pick a better starting time than now + 1 minute + long delay = 1000 * 60; // one minute, in milliseconds + mAlarmManager.set(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + delay, mRunInitIntent); + } + } + } catch (RemoteException e) { + // the transport threw when asked its file naming prefs; declare it invalid + Slog.e(TAG, "Unable to register transport as " + name); + mTransportNames.remove(component); + mTransports.remove(name); + } + } + + // ----- Track installation/removal of packages ----- + BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (DEBUG) Slog.d(TAG, "Received broadcast " + intent); + + String action = intent.getAction(); + boolean replacing = false; + boolean added = false; + Bundle extras = intent.getExtras(); + String pkgList[] = null; + if (Intent.ACTION_PACKAGE_ADDED.equals(action) || + Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + Uri uri = intent.getData(); + if (uri == null) { + return; + } + String pkgName = uri.getSchemeSpecificPart(); + if (pkgName != null) { + pkgList = new String[] { pkgName }; + } + added = Intent.ACTION_PACKAGE_ADDED.equals(action); + replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + added = true; + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + added = false; + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + } + + if (pkgList == null || pkgList.length == 0) { + return; + } + + final int uid = extras.getInt(Intent.EXTRA_UID); + if (added) { + synchronized (mBackupParticipants) { + if (replacing) { + // This is the package-replaced case; we just remove the entry + // under the old uid and fall through to re-add. + removePackageParticipantsLocked(pkgList, uid); + } + addPackageParticipantsLocked(pkgList); + } + } else { + if (replacing) { + // The package is being updated. We'll receive a PACKAGE_ADDED shortly. + } else { + synchronized (mBackupParticipants) { + removePackageParticipantsLocked(pkgList, uid); + } + } + } + } + }; + + // ----- Track connection to transports service ----- + class TransportConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (DEBUG) Slog.v(TAG, "Connected to transport " + component); + try { + IBackupTransport transport = IBackupTransport.Stub.asInterface(service); + registerTransport(transport.name(), component.flattenToShortString(), transport); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to register transport " + component); + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (DEBUG) Slog.v(TAG, "Disconnected from transport " + component); + registerTransport(null, component.flattenToShortString(), null); + } + }; + + // Add the backup agents in the given packages to our set of known backup participants. + // If 'packageNames' is null, adds all backup agents in the whole system. + void addPackageParticipantsLocked(String[] packageNames) { + // Look for apps that define the android:backupAgent attribute + List targetApps = allAgentPackages(); + if (packageNames != null) { + if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: #" + packageNames.length); + for (String packageName : packageNames) { + addPackageParticipantsLockedInner(packageName, targetApps); + } + } else { + if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: all"); + addPackageParticipantsLockedInner(null, targetApps); + } + } + + private void addPackageParticipantsLockedInner(String packageName, + List targetPkgs) { + if (MORE_DEBUG) { + Slog.v(TAG, "Examining " + packageName + " for backup agent"); + } + + for (PackageInfo pkg : targetPkgs) { + if (packageName == null || pkg.packageName.equals(packageName)) { + int uid = pkg.applicationInfo.uid; + HashSet set = mBackupParticipants.get(uid); + if (set == null) { + set = new HashSet(); + mBackupParticipants.put(uid, set); + } + set.add(pkg.packageName); + if (MORE_DEBUG) Slog.v(TAG, "Agent found; added"); + + // Schedule a backup for it on general principles + if (DEBUG) Slog.i(TAG, "Scheduling backup for new app " + pkg.packageName); + dataChangedImpl(pkg.packageName); + } + } + } + + // Remove the given packages' entries from our known active set. + void removePackageParticipantsLocked(String[] packageNames, int oldUid) { + if (packageNames == null) { + Slog.w(TAG, "removePackageParticipants with null list"); + return; + } + + if (DEBUG) Slog.v(TAG, "removePackageParticipantsLocked: uid=" + oldUid + + " #" + packageNames.length); + for (String pkg : packageNames) { + // Known previous UID, so we know which package set to check + HashSet set = mBackupParticipants.get(oldUid); + if (set != null && set.contains(pkg)) { + removePackageFromSetLocked(set, pkg); + if (set.isEmpty()) { + if (MORE_DEBUG) Slog.v(TAG, " last one of this uid; purging set"); + mBackupParticipants.remove(oldUid); + } + } + } + } + + private void removePackageFromSetLocked(final HashSet set, + final String packageName) { + if (set.contains(packageName)) { + // Found it. Remove this one package from the bookkeeping, and + // if it's the last participating app under this uid we drop the + // (now-empty) set as well. + // Note that we deliberately leave it 'known' in the "ever backed up" + // bookkeeping so that its current-dataset data will be retrieved + // if the app is subsequently reinstalled + if (MORE_DEBUG) Slog.v(TAG, " removing participant " + packageName); + set.remove(packageName); + mPendingBackups.remove(packageName); + } + } + + // Returns the set of all applications that define an android:backupAgent attribute + List allAgentPackages() { + // !!! TODO: cache this and regenerate only when necessary + int flags = PackageManager.GET_SIGNATURES; + List packages = mPackageManager.getInstalledPackages(flags); + int N = packages.size(); + for (int a = N-1; a >= 0; a--) { + PackageInfo pkg = packages.get(a); + try { + ApplicationInfo app = pkg.applicationInfo; + if (((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) + || app.backupAgentName == null) { + packages.remove(a); + } + else { + // we will need the shared library path, so look that up and store it here + app = mPackageManager.getApplicationInfo(pkg.packageName, + PackageManager.GET_SHARED_LIBRARY_FILES); + pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles; + } + } catch (NameNotFoundException e) { + packages.remove(a); + } + } + return packages; + } + + // Called from the backup task: record that the given app has been successfully + // backed up at least once + void logBackupComplete(String packageName) { + if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return; + + synchronized (mEverStoredApps) { + if (!mEverStoredApps.add(packageName)) return; + + RandomAccessFile out = null; + try { + out = new RandomAccessFile(mEverStored, "rws"); + out.seek(out.length()); + out.writeUTF(packageName); + } catch (IOException e) { + Slog.e(TAG, "Can't log backup of " + packageName + " to " + mEverStored); + } finally { + try { if (out != null) out.close(); } catch (IOException e) {} + } + } + } + + // Remove our awareness of having ever backed up the given package + void removeEverBackedUp(String packageName) { + if (DEBUG) Slog.v(TAG, "Removing backed-up knowledge of " + packageName); + if (MORE_DEBUG) Slog.v(TAG, "New set:"); + + synchronized (mEverStoredApps) { + // Rewrite the file and rename to overwrite. If we reboot in the middle, + // we'll recognize on initialization time that the package no longer + // exists and fix it up then. + File tempKnownFile = new File(mBaseStateDir, "processed.new"); + RandomAccessFile known = null; + try { + known = new RandomAccessFile(tempKnownFile, "rws"); + mEverStoredApps.remove(packageName); + for (String s : mEverStoredApps) { + known.writeUTF(s); + if (MORE_DEBUG) Slog.v(TAG, " " + s); + } + known.close(); + known = null; + if (!tempKnownFile.renameTo(mEverStored)) { + throw new IOException("Can't rename " + tempKnownFile + " to " + mEverStored); + } + } catch (IOException e) { + // Bad: we couldn't create the new copy. For safety's sake we + // abandon the whole process and remove all what's-backed-up + // state entirely, meaning we'll force a backup pass for every + // participant on the next boot or [re]install. + Slog.w(TAG, "Error rewriting " + mEverStored, e); + mEverStoredApps.clear(); + tempKnownFile.delete(); + mEverStored.delete(); + } finally { + try { if (known != null) known.close(); } catch (IOException e) {} + } + } + } + + // Persistently record the current and ancestral backup tokens as well + // as the set of packages with data [supposedly] available in the + // ancestral dataset. + void writeRestoreTokens() { + try { + RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd"); + + // First, the version number of this record, for futureproofing + af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION); + + // Write the ancestral and current tokens + af.writeLong(mAncestralToken); + af.writeLong(mCurrentToken); + + // Now write the set of ancestral packages + if (mAncestralPackages == null) { + af.writeInt(-1); + } else { + af.writeInt(mAncestralPackages.size()); + if (DEBUG) Slog.v(TAG, "Ancestral packages: " + mAncestralPackages.size()); + for (String pkgName : mAncestralPackages) { + af.writeUTF(pkgName); + if (MORE_DEBUG) Slog.v(TAG, " " + pkgName); + } + } + af.close(); + } catch (IOException e) { + Slog.w(TAG, "Unable to write token file:", e); + } + } + + // Return the given transport + private IBackupTransport getTransport(String transportName) { + synchronized (mTransports) { + IBackupTransport transport = mTransports.get(transportName); + if (transport == null) { + Slog.w(TAG, "Requested unavailable transport: " + transportName); + } + return transport; + } + } + + // fire off a backup agent, blocking until it attaches or times out + IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { + IBackupAgent agent = null; + synchronized(mAgentConnectLock) { + mConnecting = true; + mConnectedAgent = null; + try { + if (mActivityManager.bindBackupAgent(app, mode)) { + Slog.d(TAG, "awaiting agent for " + app); + + // success; wait for the agent to arrive + // only wait 10 seconds for the bind to happen + long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; + while (mConnecting && mConnectedAgent == null + && (System.currentTimeMillis() < timeoutMark)) { + try { + mAgentConnectLock.wait(5000); + } catch (InterruptedException e) { + // just bail + if (DEBUG) Slog.w(TAG, "Interrupted: " + e); + mActivityManager.clearPendingBackup(); + return null; + } + } + + // if we timed out with no connect, abort and move on + if (mConnecting == true) { + Slog.w(TAG, "Timeout waiting for agent " + app); + mActivityManager.clearPendingBackup(); + return null; + } + if (DEBUG) Slog.i(TAG, "got agent " + mConnectedAgent); + agent = mConnectedAgent; + } + } catch (RemoteException e) { + // can't happen - ActivityManager is local + } + } + return agent; + } + + // clear an application's data, blocking until the operation completes or times out + void clearApplicationDataSynchronous(String packageName) { + // Don't wipe packages marked allowClearUserData=false + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) == 0) { + if (MORE_DEBUG) Slog.i(TAG, "allowClearUserData=false so not wiping " + + packageName); + return; + } + } catch (NameNotFoundException e) { + Slog.w(TAG, "Tried to clear data for " + packageName + " but not found"); + return; + } + + ClearDataObserver observer = new ClearDataObserver(); + + synchronized(mClearDataLock) { + mClearingData = true; + try { + mActivityManager.clearApplicationUserData(packageName, observer, 0); + } catch (RemoteException e) { + // can't happen because the activity manager is in this process + } + + // only wait 10 seconds for the clear data to happen + long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; + while (mClearingData && (System.currentTimeMillis() < timeoutMark)) { + try { + mClearDataLock.wait(5000); + } catch (InterruptedException e) { + // won't happen, but still. + mClearingData = false; + } + } + } + } + + class ClearDataObserver extends IPackageDataObserver.Stub { + public void onRemoveCompleted(String packageName, boolean succeeded) { + synchronized(mClearDataLock) { + mClearingData = false; + mClearDataLock.notifyAll(); + } + } + } + + // Get the restore-set token for the best-available restore set for this package: + // the active set if possible, else the ancestral one. Returns zero if none available. + long getAvailableRestoreToken(String packageName) { + long token = mAncestralToken; + synchronized (mQueueLock) { + if (mEverStoredApps.contains(packageName)) { + token = mCurrentToken; + } + } + return token; + } + + // ----- + // Interface and methods used by the asynchronous-with-timeout backup/restore operations + + interface BackupRestoreTask { + // Execute one tick of whatever state machine the task implements + void execute(); + + // An operation that wanted a callback has completed + void operationComplete(); + + // An operation that wanted a callback has timed out + void handleTimeout(); + } + + void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback) { + if (MORE_DEBUG) Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token) + + " interval=" + interval); + synchronized (mCurrentOpLock) { + mCurrentOperations.put(token, new Operation(OP_PENDING, callback)); + + Message msg = mBackupHandler.obtainMessage(MSG_TIMEOUT, token, 0, callback); + mBackupHandler.sendMessageDelayed(msg, interval); + } + } + + // synchronous waiter case + boolean waitUntilOperationComplete(int token) { + if (MORE_DEBUG) Slog.i(TAG, "Blocking until operation complete for " + + Integer.toHexString(token)); + int finalState = OP_PENDING; + Operation op = null; + synchronized (mCurrentOpLock) { + while (true) { + op = mCurrentOperations.get(token); + if (op == null) { + // mysterious disappearance: treat as success with no callback + break; + } else { + if (op.state == OP_PENDING) { + try { + mCurrentOpLock.wait(); + } catch (InterruptedException e) {} + // When the wait is notified we loop around and recheck the current state + } else { + // No longer pending; we're done + finalState = op.state; + break; + } + } + } + } + + mBackupHandler.removeMessages(MSG_TIMEOUT); + if (MORE_DEBUG) Slog.v(TAG, "operation " + Integer.toHexString(token) + + " complete: finalState=" + finalState); + return finalState == OP_ACKNOWLEDGED; + } + + void handleTimeout(int token, Object obj) { + // Notify any synchronous waiters + Operation op = null; + synchronized (mCurrentOpLock) { + op = mCurrentOperations.get(token); + if (MORE_DEBUG) { + if (op == null) Slog.w(TAG, "Timeout of token " + Integer.toHexString(token) + + " but no op found"); + } + int state = (op != null) ? op.state : OP_TIMEOUT; + if (state == OP_PENDING) { + if (DEBUG) Slog.v(TAG, "TIMEOUT: token=" + Integer.toHexString(token)); + op.state = OP_TIMEOUT; + mCurrentOperations.put(token, op); + } + mCurrentOpLock.notifyAll(); + } + + // If there's a TimeoutHandler for this event, call it + if (op != null && op.callback != null) { + op.callback.handleTimeout(); + } + } + + // ----- Back up a set of applications via a worker thread ----- + + enum BackupState { + INITIAL, + RUNNING_QUEUE, + FINAL + } + + class PerformBackupTask implements BackupRestoreTask { + private static final String TAG = "PerformBackupTask"; + + IBackupTransport mTransport; + ArrayList mQueue; + ArrayList mOriginalQueue; + File mStateDir; + File mJournal; + BackupState mCurrentState; + + // carried information about the current in-flight operation + PackageInfo mCurrentPackage; + File mSavedStateName; + File mBackupDataName; + File mNewStateName; + ParcelFileDescriptor mSavedState; + ParcelFileDescriptor mBackupData; + ParcelFileDescriptor mNewState; + int mStatus; + boolean mFinished; + + public PerformBackupTask(IBackupTransport transport, String dirName, + ArrayList queue, File journal) { + mTransport = transport; + mOriginalQueue = queue; + mJournal = journal; + + mStateDir = new File(mBaseStateDir, dirName); + + mCurrentState = BackupState.INITIAL; + mFinished = false; + + addBackupTrace("STATE => INITIAL"); + } + + // Main entry point: perform one chunk of work, updating the state as appropriate + // and reposting the next chunk to the primary backup handler thread. + @Override + public void execute() { + switch (mCurrentState) { + case INITIAL: + beginBackup(); + break; + + case RUNNING_QUEUE: + invokeNextAgent(); + break; + + case FINAL: + if (!mFinished) finalizeBackup(); + else { + Slog.e(TAG, "Duplicate finish"); + } + mFinished = true; + break; + } + } + + // We're starting a backup pass. Initialize the transport and send + // the PM metadata blob if we haven't already. + void beginBackup() { + if (DEBUG_BACKUP_TRACE) { + clearBackupTrace(); + StringBuilder b = new StringBuilder(256); + b.append("beginBackup: ["); + for (BackupRequest req : mOriginalQueue) { + b.append(' '); + b.append(req.packageName); + } + b.append(" ]"); + addBackupTrace(b.toString()); + } + + mStatus = BackupConstants.TRANSPORT_OK; + + // Sanity check: if the queue is empty we have no work to do. + if (mOriginalQueue.isEmpty()) { + Slog.w(TAG, "Backup begun with an empty queue - nothing to do."); + addBackupTrace("queue empty at begin"); + executeNextState(BackupState.FINAL); + return; + } + + // We need to retain the original queue contents in case of transport + // failure, but we want a working copy that we can manipulate along + // the way. + mQueue = (ArrayList) mOriginalQueue.clone(); + + if (DEBUG) Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); + + File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + try { + final String transportName = mTransport.transportDirName(); + EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); + + // If we haven't stored package manager metadata yet, we must init the transport. + if (mStatus == BackupConstants.TRANSPORT_OK && pmState.length() <= 0) { + Slog.i(TAG, "Initializing (wiping) backup state and transport storage"); + addBackupTrace("initializing transport " + transportName); + resetBackupState(mStateDir); // Just to make sure. + mStatus = mTransport.initializeDevice(); + + addBackupTrace("transport.initializeDevice() == " + mStatus); + if (mStatus == BackupConstants.TRANSPORT_OK) { + EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); + } else { + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + Slog.e(TAG, "Transport error in initializeDevice()"); + } + } + + // The package manager doesn't have a proper etc, but since + // it's running here in the system process we can just set up its agent + // directly and use a synthetic BackupRequest. We always run this pass + // because it's cheap and this way we guarantee that we don't get out of + // step even if we're selecting among various transports at run time. + if (mStatus == BackupConstants.TRANSPORT_OK) { + PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent( + mPackageManager, allAgentPackages()); + mStatus = invokeAgentForBackup(PACKAGE_MANAGER_SENTINEL, + IBackupAgent.Stub.asInterface(pmAgent.onBind()), mTransport); + addBackupTrace("PMBA invoke: " + mStatus); + } + + if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { + // The backend reports that our dataset has been wiped. Note this in + // the event log; the no-success code below will reset the backup + // state as well. + EventLog.writeEvent(EventLogTags.BACKUP_RESET, mTransport.transportDirName()); + } + } catch (Exception e) { + Slog.e(TAG, "Error in backup thread", e); + addBackupTrace("Exception in backup thread: " + e); + mStatus = BackupConstants.TRANSPORT_ERROR; + } finally { + // If we've succeeded so far, invokeAgentForBackup() will have run the PM + // metadata and its completion/timeout callback will continue the state + // machine chain. If it failed that won't happen; we handle that now. + addBackupTrace("exiting prelim: " + mStatus); + if (mStatus != BackupConstants.TRANSPORT_OK) { + // if things went wrong at this point, we need to + // restage everything and try again later. + resetBackupState(mStateDir); // Just to make sure. + executeNextState(BackupState.FINAL); + } + } + } + + // Transport has been initialized and the PM metadata submitted successfully + // if that was warranted. Now we process the single next thing in the queue. + void invokeNextAgent() { + mStatus = BackupConstants.TRANSPORT_OK; + addBackupTrace("invoke q=" + mQueue.size()); + + // Sanity check that we have work to do. If not, skip to the end where + // we reestablish the wakelock invariants etc. + if (mQueue.isEmpty()) { + if (DEBUG) Slog.i(TAG, "queue now empty"); + executeNextState(BackupState.FINAL); + return; + } + + // pop the entry we're going to process on this step + BackupRequest request = mQueue.get(0); + mQueue.remove(0); + + Slog.d(TAG, "starting agent for backup of " + request); + addBackupTrace("launch agent for " + request.packageName); + + // Verify that the requested app exists; it might be something that + // requested a backup but was then uninstalled. The request was + // journalled and rather than tamper with the journal it's safer + // to sanity-check here. This also gives us the classname of the + // package's backup agent. + try { + mCurrentPackage = mPackageManager.getPackageInfo(request.packageName, + PackageManager.GET_SIGNATURES); + if (mCurrentPackage.applicationInfo.backupAgentName == null) { + // The manifest has changed but we had a stale backup request pending. + // This won't happen again because the app won't be requesting further + // backups. + Slog.i(TAG, "Package " + request.packageName + + " no longer supports backup; skipping"); + addBackupTrace("skipping - no agent, completion is noop"); + executeNextState(BackupState.RUNNING_QUEUE); + return; + } + + if ((mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { + // The app has been force-stopped or cleared or just installed, + // and not yet launched out of that state, so just as it won't + // receive broadcasts, we won't run it for backup. + addBackupTrace("skipping - stopped"); + executeNextState(BackupState.RUNNING_QUEUE); + return; + } + + IBackupAgent agent = null; + try { + mWakelock.setWorkSource(new WorkSource(mCurrentPackage.applicationInfo.uid)); + agent = bindToAgentSynchronous(mCurrentPackage.applicationInfo, + IApplicationThread.BACKUP_MODE_INCREMENTAL); + addBackupTrace("agent bound; a? = " + (agent != null)); + if (agent != null) { + mStatus = invokeAgentForBackup(request.packageName, agent, mTransport); + // at this point we'll either get a completion callback from the + // agent, or a timeout message on the main handler. either way, we're + // done here as long as we're successful so far. + } else { + // Timeout waiting for the agent + mStatus = BackupConstants.AGENT_ERROR; + } + } catch (SecurityException ex) { + // Try for the next one. + Slog.d(TAG, "error in bind/backup", ex); + mStatus = BackupConstants.AGENT_ERROR; + addBackupTrace("agent SE"); + } + } catch (NameNotFoundException e) { + Slog.d(TAG, "Package does not exist; skipping"); + addBackupTrace("no such package"); + mStatus = BackupConstants.AGENT_UNKNOWN; + } finally { + mWakelock.setWorkSource(null); + + // If there was an agent error, no timeout/completion handling will occur. + // That means we need to direct to the next state ourselves. + if (mStatus != BackupConstants.TRANSPORT_OK) { + BackupState nextState = BackupState.RUNNING_QUEUE; + + // An agent-level failure means we reenqueue this one agent for + // a later retry, but otherwise proceed normally. + if (mStatus == BackupConstants.AGENT_ERROR) { + if (MORE_DEBUG) Slog.i(TAG, "Agent failure for " + request.packageName + + " - restaging"); + dataChangedImpl(request.packageName); + mStatus = BackupConstants.TRANSPORT_OK; + if (mQueue.isEmpty()) nextState = BackupState.FINAL; + } else if (mStatus == BackupConstants.AGENT_UNKNOWN) { + // Failed lookup of the app, so we couldn't bring up an agent, but + // we're otherwise fine. Just drop it and go on to the next as usual. + mStatus = BackupConstants.TRANSPORT_OK; + } else { + // Transport-level failure means we reenqueue everything + revertAndEndBackup(); + nextState = BackupState.FINAL; + } + + executeNextState(nextState); + } else { + addBackupTrace("expecting completion/timeout callback"); + } + } + } + + void finalizeBackup() { + addBackupTrace("finishing"); + + // Either backup was successful, in which case we of course do not need + // this pass's journal any more; or it failed, in which case we just + // re-enqueued all of these packages in the current active journal. + // Either way, we no longer need this pass's journal. + if (mJournal != null && !mJournal.delete()) { + Slog.e(TAG, "Unable to remove backup journal file " + mJournal); + } + + // If everything actually went through and this is the first time we've + // done a backup, we can now record what the current backup dataset token + // is. + if ((mCurrentToken == 0) && (mStatus == BackupConstants.TRANSPORT_OK)) { + addBackupTrace("success; recording token"); + try { + mCurrentToken = mTransport.getCurrentRestoreSet(); + writeRestoreTokens(); + } catch (RemoteException e) { + // nothing for it at this point, unfortunately, but this will be + // recorded the next time we fully succeed. + addBackupTrace("transport threw returning token"); + } + } + + // Set up the next backup pass - at this point we can set mBackupRunning + // to false to allow another pass to fire, because we're done with the + // state machine sequence and the wakelock is refcounted. + synchronized (mQueueLock) { + mBackupRunning = false; + if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { + // Make sure we back up everything and perform the one-time init + clearMetadata(); + if (DEBUG) Slog.d(TAG, "Server requires init; rerunning"); + addBackupTrace("init required; rerunning"); + backupNow(); + } + } + + // Only once we're entirely finished do we release the wakelock + clearBackupTrace(); + Slog.i(TAG, "Backup pass finished."); + mWakelock.release(); + } + + // Remove the PM metadata state. This will generate an init on the next pass. + void clearMetadata() { + final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + if (pmState.exists()) pmState.delete(); + } + + // Invoke an agent's doBackup() and start a timeout message spinning on the main + // handler in case it doesn't get back to us. + int invokeAgentForBackup(String packageName, IBackupAgent agent, + IBackupTransport transport) { + if (DEBUG) Slog.d(TAG, "invokeAgentForBackup on " + packageName); + addBackupTrace("invoking " + packageName); + + mSavedStateName = new File(mStateDir, packageName); + mBackupDataName = new File(mDataDir, packageName + ".data"); + mNewStateName = new File(mStateDir, packageName + ".new"); + + mSavedState = null; + mBackupData = null; + mNewState = null; + + final int token = generateToken(); + try { + // Look up the package info & signatures. This is first so that if it + // throws an exception, there's no file setup yet that would need to + // be unraveled. + if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) { + // The metadata 'package' is synthetic; construct one and make + // sure our global state is pointed at it + mCurrentPackage = new PackageInfo(); + mCurrentPackage.packageName = packageName; + } + + // In a full backup, we pass a null ParcelFileDescriptor as + // the saved-state "file". This is by definition an incremental, + // so we build a saved state file to pass. + mSavedState = ParcelFileDescriptor.open(mSavedStateName, + ParcelFileDescriptor.MODE_READ_ONLY | + ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary + + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName); + } + + mNewState = ParcelFileDescriptor.open(mNewStateName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + // Initiate the target's backup pass + addBackupTrace("setting timeout"); + prepareOperationTimeout(token, TIMEOUT_BACKUP_INTERVAL, this); + addBackupTrace("calling agent doBackup()"); + agent.doBackup(mSavedState, mBackupData, mNewState, token, mBackupManagerBinder); + } catch (Exception e) { + Slog.e(TAG, "Error invoking for backup on " + packageName); + addBackupTrace("exception: " + e); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, + e.toString()); + agentErrorCleanup(); + return BackupConstants.AGENT_ERROR; + } + + // At this point the agent is off and running. The next thing to happen will + // either be a callback from the agent, at which point we'll process its data + // for transport, or a timeout. Either way the next phase will happen in + // response to the TimeoutHandler interface callbacks. + addBackupTrace("invoke success"); + return BackupConstants.TRANSPORT_OK; + } + + @Override + public void operationComplete() { + // Okay, the agent successfully reported back to us. Spin the data off to the + // transport and proceed with the next stage. + if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for " + + mCurrentPackage.packageName); + mBackupHandler.removeMessages(MSG_TIMEOUT); + clearAgentState(); + addBackupTrace("operation complete"); + + ParcelFileDescriptor backupData = null; + mStatus = BackupConstants.TRANSPORT_OK; + try { + int size = (int) mBackupDataName.length(); + if (size > 0) { + if (mStatus == BackupConstants.TRANSPORT_OK) { + backupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_ONLY); + addBackupTrace("sending data to transport"); + mStatus = mTransport.performBackup(mCurrentPackage, backupData); + } + + // TODO - We call finishBackup() for each application backed up, because + // we need to know now whether it succeeded or failed. Instead, we should + // hold off on finishBackup() until the end, which implies holding off on + // renaming *all* the output state files (see below) until that happens. + + addBackupTrace("data delivered: " + mStatus); + if (mStatus == BackupConstants.TRANSPORT_OK) { + addBackupTrace("finishing op on transport"); + mStatus = mTransport.finishBackup(); + addBackupTrace("finished: " + mStatus); + } + } else { + if (DEBUG) Slog.i(TAG, "no backup data written; not calling transport"); + addBackupTrace("no data to send"); + } + + // After successful transport, delete the now-stale data + // and juggle the files so that next time we supply the agent + // with the new state file it just created. + if (mStatus == BackupConstants.TRANSPORT_OK) { + mBackupDataName.delete(); + mNewStateName.renameTo(mSavedStateName); + EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, + mCurrentPackage.packageName, size); + logBackupComplete(mCurrentPackage.packageName); + } else { + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, + mCurrentPackage.packageName); + } + } catch (Exception e) { + Slog.e(TAG, "Transport error backing up " + mCurrentPackage.packageName, e); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, + mCurrentPackage.packageName); + mStatus = BackupConstants.TRANSPORT_ERROR; + } finally { + try { if (backupData != null) backupData.close(); } catch (IOException e) {} + } + + // If we encountered an error here it's a transport-level failure. That + // means we need to halt everything and reschedule everything for next time. + final BackupState nextState; + if (mStatus != BackupConstants.TRANSPORT_OK) { + revertAndEndBackup(); + nextState = BackupState.FINAL; + } else { + // Success! Proceed with the next app if any, otherwise we're done. + nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE; + } + + executeNextState(nextState); + } + + @Override + public void handleTimeout() { + // Whoops, the current agent timed out running doBackup(). Tidy up and restage + // it for the next time we run a backup pass. + // !!! TODO: keep track of failure counts per agent, and blacklist those which + // fail repeatedly (i.e. have proved themselves to be buggy). + Slog.e(TAG, "Timeout backing up " + mCurrentPackage.packageName); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, mCurrentPackage.packageName, + "timeout"); + addBackupTrace("timeout of " + mCurrentPackage.packageName); + agentErrorCleanup(); + dataChangedImpl(mCurrentPackage.packageName); + } + + void revertAndEndBackup() { + if (MORE_DEBUG) Slog.i(TAG, "Reverting backup queue - restaging everything"); + addBackupTrace("transport error; reverting"); + for (BackupRequest request : mOriginalQueue) { + dataChangedImpl(request.packageName); + } + // We also want to reset the backup schedule based on whatever + // the transport suggests by way of retry/backoff time. + restartBackupAlarm(); + } + + void agentErrorCleanup() { + mBackupDataName.delete(); + mNewStateName.delete(); + clearAgentState(); + + executeNextState(mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE); + } + + // Cleanup common to both success and failure cases + void clearAgentState() { + try { if (mSavedState != null) mSavedState.close(); } catch (IOException e) {} + try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} + try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} + mSavedState = mBackupData = mNewState = null; + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + + // If this was a pseudopackage there's no associated Activity Manager state + if (mCurrentPackage.applicationInfo != null) { + addBackupTrace("unbinding " + mCurrentPackage.packageName); + try { // unbind even on timeout, just in case + mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); + } catch (RemoteException e) { /* can't happen; activity manager is local */ } + } + } + + void restartBackupAlarm() { + addBackupTrace("setting backup trigger"); + synchronized (mQueueLock) { + try { + startBackupAlarmsLocked(mTransport.requestBackupTime()); + } catch (RemoteException e) { /* cannot happen */ } + } + } + + void executeNextState(BackupState nextState) { + if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " + + this + " nextState=" + nextState); + addBackupTrace("executeNextState => " + nextState); + mCurrentState = nextState; + Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); + mBackupHandler.sendMessage(msg); + } + } + + + // ----- Full backup/restore to a file/socket ----- + + abstract class ObbServiceClient { + public IObbBackupService mObbService; + public void setObbBinder(IObbBackupService binder) { + mObbService = binder; + } + } + + class FullBackupObbConnection implements ServiceConnection { + volatile IObbBackupService mService; + + FullBackupObbConnection() { + mService = null; + } + + public void establish() { + if (DEBUG) Slog.i(TAG, "Initiating bind of OBB service on " + this); + Intent obbIntent = new Intent().setComponent(new ComponentName( + "com.android.sharedstoragebackup", + "com.android.sharedstoragebackup.ObbBackupService")); + BackupManagerService.this.mContext.bindService( + obbIntent, this, Context.BIND_AUTO_CREATE); + } + + public void tearDown() { + BackupManagerService.this.mContext.unbindService(this); + } + + public boolean backupObbs(PackageInfo pkg, OutputStream out) { + boolean success = false; + waitForConnection(); + + ParcelFileDescriptor[] pipes = null; + try { + pipes = ParcelFileDescriptor.createPipe(); + int token = generateToken(); + prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); + mService.backupObbs(pkg.packageName, pipes[1], token, mBackupManagerBinder); + routeSocketDataToOutput(pipes[0], out); + success = waitUntilOperationComplete(token); + } catch (Exception e) { + Slog.w(TAG, "Unable to back up OBBs for " + pkg, e); + } finally { + try { + out.flush(); + if (pipes != null) { + if (pipes[0] != null) pipes[0].close(); + if (pipes[1] != null) pipes[1].close(); + } + } catch (IOException e) { + Slog.w(TAG, "I/O error closing down OBB backup", e); + } + } + return success; + } + + public void restoreObbFile(String pkgName, ParcelFileDescriptor data, + long fileSize, int type, String path, long mode, long mtime, + int token, IBackupManager callbackBinder) { + waitForConnection(); + + try { + mService.restoreObbFile(pkgName, data, fileSize, type, path, mode, mtime, + token, callbackBinder); + } catch (Exception e) { + Slog.w(TAG, "Unable to restore OBBs for " + pkgName, e); + } + } + + private void waitForConnection() { + synchronized (this) { + while (mService == null) { + if (DEBUG) Slog.i(TAG, "...waiting for OBB service binding..."); + try { + this.wait(); + } catch (InterruptedException e) { /* never interrupted */ } + } + if (DEBUG) Slog.i(TAG, "Connected to OBB service; continuing"); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (this) { + mService = IObbBackupService.Stub.asInterface(service); + if (DEBUG) Slog.i(TAG, "OBB service connection " + mService + + " connected on " + this); + this.notifyAll(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (this) { + mService = null; + if (DEBUG) Slog.i(TAG, "OBB service connection disconnected on " + this); + this.notifyAll(); + } + } + + } + + private void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out) + throws IOException { + FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor()); + DataInputStream in = new DataInputStream(raw); + + byte[] buffer = new byte[32 * 1024]; + int chunkTotal; + while ((chunkTotal = in.readInt()) > 0) { + while (chunkTotal > 0) { + int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal; + int nRead = in.read(buffer, 0, toRead); + out.write(buffer, 0, nRead); + chunkTotal -= nRead; + } + } + } + + class PerformFullBackupTask extends ObbServiceClient implements Runnable { + ParcelFileDescriptor mOutputFile; + DeflaterOutputStream mDeflater; + IFullBackupRestoreObserver mObserver; + boolean mIncludeApks; + boolean mIncludeObbs; + boolean mIncludeShared; + boolean mAllApps; + final boolean mIncludeSystem; + String[] mPackages; + String mCurrentPassword; + String mEncryptPassword; + AtomicBoolean mLatchObject; + File mFilesDir; + File mManifestFile; + + + class FullBackupRunner implements Runnable { + PackageInfo mPackage; + IBackupAgent mAgent; + ParcelFileDescriptor mPipe; + int mToken; + boolean mSendApk; + boolean mWriteManifest; + + FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe, + int token, boolean sendApk, boolean writeManifest) throws IOException { + mPackage = pack; + mAgent = agent; + mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor()); + mToken = token; + mSendApk = sendApk; + mWriteManifest = writeManifest; + } + + @Override + public void run() { + try { + BackupDataOutput output = new BackupDataOutput( + mPipe.getFileDescriptor()); + + if (mWriteManifest) { + if (MORE_DEBUG) Slog.d(TAG, "Writing manifest for " + mPackage.packageName); + writeAppManifest(mPackage, mManifestFile, mSendApk); + FullBackup.backupToTar(mPackage.packageName, null, null, + mFilesDir.getAbsolutePath(), + mManifestFile.getAbsolutePath(), + output); + } + + if (mSendApk) { + writeApkToBackup(mPackage, output); + } + + if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName); + prepareOperationTimeout(mToken, TIMEOUT_FULL_BACKUP_INTERVAL, null); + mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder); + } catch (IOException e) { + Slog.e(TAG, "Error running full backup for " + mPackage.packageName); + } catch (RemoteException e) { + Slog.e(TAG, "Remote agent vanished during full backup of " + + mPackage.packageName); + } finally { + try { + mPipe.close(); + } catch (IOException e) {} + } + } + } + + PerformFullBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer, + boolean includeApks, boolean includeObbs, boolean includeShared, + String curPassword, String encryptPassword, boolean doAllApps, + boolean doSystem, String[] packages, AtomicBoolean latch) { + mOutputFile = fd; + mObserver = observer; + mIncludeApks = includeApks; + mIncludeObbs = includeObbs; + mIncludeShared = includeShared; + mAllApps = doAllApps; + mIncludeSystem = doSystem; + mPackages = packages; + mCurrentPassword = curPassword; + // when backing up, if there is a current backup password, we require that + // the user use a nonempty encryption password as well. if one is supplied + // in the UI we use that, but if the UI was left empty we fall back to the + // current backup password (which was supplied by the user as well). + if (encryptPassword == null || "".equals(encryptPassword)) { + mEncryptPassword = curPassword; + } else { + mEncryptPassword = encryptPassword; + } + mLatchObject = latch; + + mFilesDir = new File("/data/system"); + mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME); + } + + @Override + public void run() { + Slog.i(TAG, "--- Performing full-dataset backup ---"); + + List packagesToBackup = new ArrayList(); + FullBackupObbConnection obbConnection = new FullBackupObbConnection(); + obbConnection.establish(); // we'll want this later + + sendStartBackup(); + + // doAllApps supersedes the package set if any + if (mAllApps) { + packagesToBackup = mPackageManager.getInstalledPackages( + PackageManager.GET_SIGNATURES); + // Exclude system apps if we've been asked to do so + if (mIncludeSystem == false) { + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + packagesToBackup.remove(i); + } else { + i++; + } + } + } + } + + // Now process the command line argument packages, if any. Note that explicitly- + // named system-partition packages will be included even if includeSystem was + // set to false. + if (mPackages != null) { + for (String pkgName : mPackages) { + try { + packagesToBackup.add(mPackageManager.getPackageInfo(pkgName, + PackageManager.GET_SIGNATURES)); + } catch (NameNotFoundException e) { + Slog.w(TAG, "Unknown package " + pkgName + ", skipping"); + } + } + } + + // Cull any packages that have indicated that backups are not permitted, as well + // as any explicit mention of the 'special' shared-storage agent package (we + // handle that one at the end). + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0 + || pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE)) { + packagesToBackup.remove(i); + } else { + i++; + } + } + + // Cull any packages that run as system-domain uids but do not define their + // own backup agents + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) + && (pkg.applicationInfo.backupAgentName == null)) { + if (MORE_DEBUG) { + Slog.i(TAG, "... ignoring non-agent system package " + pkg.packageName); + } + packagesToBackup.remove(i); + } else { + i++; + } + } + + FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor()); + OutputStream out = null; + + PackageInfo pkg = null; + try { + boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0); + boolean compressing = COMPRESS_FULL_BACKUPS; + OutputStream finalOutput = ofstream; + + // Verify that the given password matches the currently-active + // backup password, if any + if (hasBackupPassword()) { + if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; + } + } + + // Write the global file header. All strings are UTF-8 encoded; lines end + // with a '\n' byte. Actual backup data begins immediately following the + // final '\n'. + // + // line 1: "ANDROID BACKUP" + // line 2: backup file format version, currently "1" + // line 3: compressed? "0" if not compressed, "1" if compressed. + // line 4: name of encryption algorithm [currently only "none" or "AES-256"] + // + // When line 4 is not "none", then additional header data follows: + // + // line 5: user password salt [hex] + // line 6: master key checksum salt [hex] + // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal] + // line 8: IV of the user key [hex] + // line 9: master key blob [hex] + // IV of the master key, master key itself, master key checksum hash + // + // The master key checksum is the master key plus its checksum salt, run through + // 10k rounds of PBKDF2. This is used to verify that the user has supplied the + // correct password for decrypting the archive: the master key decrypted from + // the archive using the user-supplied password is also run through PBKDF2 in + // this way, and if the result does not match the checksum as stored in the + // archive, then we know that the user-supplied password does not match the + // archive's. + StringBuilder headerbuf = new StringBuilder(1024); + + headerbuf.append(BACKUP_FILE_HEADER_MAGIC); + headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n + headerbuf.append(compressing ? "\n1\n" : "\n0\n"); + + try { + // Set up the encryption stage if appropriate, and emit the correct header + if (encrypting) { + finalOutput = emitAesBackupHeader(headerbuf, finalOutput); + } else { + headerbuf.append("none\n"); + } + + byte[] header = headerbuf.toString().getBytes("UTF-8"); + ofstream.write(header); + + // Set up the compression stage feeding into the encryption stage (if any) + if (compressing) { + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + finalOutput = new DeflaterOutputStream(finalOutput, deflater, true); + } + + out = finalOutput; + } catch (Exception e) { + // Should never happen! + Slog.e(TAG, "Unable to emit archive header", e); + return; + } + + // Shared storage if requested + if (mIncludeShared) { + try { + pkg = mPackageManager.getPackageInfo(SHARED_BACKUP_AGENT_PACKAGE, 0); + packagesToBackup.add(pkg); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Unable to find shared-storage backup handler"); + } + } + + // Now back up the app data via the agent mechanism + int N = packagesToBackup.size(); + for (int i = 0; i < N; i++) { + pkg = packagesToBackup.get(i); + backupOnePackage(pkg, out); + + // after the app's agent runs to handle its private filesystem + // contents, back up any OBB content it has on its behalf. + if (mIncludeObbs) { + boolean obbOkay = obbConnection.backupObbs(pkg, out); + if (!obbOkay) { + throw new RuntimeException("Failure writing OBB stack for " + pkg); + } + } + } + + // Done! + finalizeBackup(out); + } catch (RemoteException e) { + Slog.e(TAG, "App died during full backup"); + } catch (Exception e) { + Slog.e(TAG, "Internal exception during full backup", e); + } finally { + tearDown(pkg); + try { + if (out != null) out.close(); + mOutputFile.close(); + } catch (IOException e) { + /* nothing we can do about this */ + } + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + synchronized (mLatchObject) { + mLatchObject.set(true); + mLatchObject.notifyAll(); + } + sendEndBackup(); + obbConnection.tearDown(); + if (DEBUG) Slog.d(TAG, "Full backup pass complete."); + mWakelock.release(); + } + } + + private OutputStream emitAesBackupHeader(StringBuilder headerbuf, + OutputStream ofstream) throws Exception { + // User key will be used to encrypt the master key. + byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); + SecretKey userKey = buildPasswordKey(mEncryptPassword, newUserSalt, + PBKDF2_HASH_ROUNDS); + + // the master key is random for each backup + byte[] masterPw = new byte[256 / 8]; + mRng.nextBytes(masterPw); + byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE); + + // primary encryption of the datastream with the random key + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES"); + c.init(Cipher.ENCRYPT_MODE, masterKeySpec); + OutputStream finalOutput = new CipherOutputStream(ofstream, c); + + // line 4: name of encryption algorithm + headerbuf.append(ENCRYPTION_ALGORITHM_NAME); + headerbuf.append('\n'); + // line 5: user password salt [hex] + headerbuf.append(byteArrayToHex(newUserSalt)); + headerbuf.append('\n'); + // line 6: master key checksum salt [hex] + headerbuf.append(byteArrayToHex(checksumSalt)); + headerbuf.append('\n'); + // line 7: number of PBKDF2 rounds used [decimal] + headerbuf.append(PBKDF2_HASH_ROUNDS); + headerbuf.append('\n'); + + // line 8: IV of the user key [hex] + Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding"); + mkC.init(Cipher.ENCRYPT_MODE, userKey); + + byte[] IV = mkC.getIV(); + headerbuf.append(byteArrayToHex(IV)); + headerbuf.append('\n'); + + // line 9: master IV + key blob, encrypted by the user key [hex]. Blob format: + // [byte] IV length = Niv + // [array of Niv bytes] IV itself + // [byte] master key length = Nmk + // [array of Nmk bytes] master key itself + // [byte] MK checksum hash length = Nck + // [array of Nck bytes] master key checksum hash + // + // The checksum is the (master key + checksum salt), run through the + // stated number of PBKDF2 rounds + IV = c.getIV(); + byte[] mk = masterKeySpec.getEncoded(); + byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(), + checksumSalt, PBKDF2_HASH_ROUNDS); + + ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length + + checksum.length + 3); + DataOutputStream mkOut = new DataOutputStream(blob); + mkOut.writeByte(IV.length); + mkOut.write(IV); + mkOut.writeByte(mk.length); + mkOut.write(mk); + mkOut.writeByte(checksum.length); + mkOut.write(checksum); + mkOut.flush(); + byte[] encryptedMk = mkC.doFinal(blob.toByteArray()); + headerbuf.append(byteArrayToHex(encryptedMk)); + headerbuf.append('\n'); + + return finalOutput; + } + + private void backupOnePackage(PackageInfo pkg, OutputStream out) + throws RemoteException { + Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName); + + IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo, + IApplicationThread.BACKUP_MODE_FULL); + if (agent != null) { + ParcelFileDescriptor[] pipes = null; + try { + pipes = ParcelFileDescriptor.createPipe(); + + ApplicationInfo app = pkg.applicationInfo; + final boolean isSharedStorage = pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE); + final boolean sendApk = mIncludeApks + && !isSharedStorage + && ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) == 0) + && ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 || + (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); + + sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName); + + final int token = generateToken(); + FullBackupRunner runner = new FullBackupRunner(pkg, agent, pipes[1], + token, sendApk, !isSharedStorage); + pipes[1].close(); // the runner has dup'd it + pipes[1] = null; + Thread t = new Thread(runner); + t.start(); + + // Now pull data from the app and stuff it into the compressor + try { + routeSocketDataToOutput(pipes[0], out); + } catch (IOException e) { + Slog.i(TAG, "Caught exception reading from agent", e); + } + + if (!waitUntilOperationComplete(token)) { + Slog.e(TAG, "Full backup failed on package " + pkg.packageName); + } else { + if (DEBUG) Slog.d(TAG, "Full package backup success: " + pkg.packageName); + } + + } catch (IOException e) { + Slog.e(TAG, "Error backing up " + pkg.packageName, e); + } finally { + try { + // flush after every package + out.flush(); + if (pipes != null) { + if (pipes[0] != null) pipes[0].close(); + if (pipes[1] != null) pipes[1].close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error bringing down backup stack"); + } + } + } else { + Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName); + } + tearDown(pkg); + } + + private void writeApkToBackup(PackageInfo pkg, BackupDataOutput output) { + // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here + final String appSourceDir = pkg.applicationInfo.sourceDir; + final String apkDir = new File(appSourceDir).getParent(); + FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null, + apkDir, appSourceDir, output); + + // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM + // doesn't have access to external storage. + + // Save associated .obb content if it exists and we did save the apk + // check for .obb and save those too + final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_OWNER); + final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0]; + if (obbDir != null) { + if (MORE_DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); + File[] obbFiles = obbDir.listFiles(); + if (obbFiles != null) { + final String obbDirName = obbDir.getAbsolutePath(); + for (File obb : obbFiles) { + FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null, + obbDirName, obb.getAbsolutePath(), output); + } + } + } + } + + private void finalizeBackup(OutputStream out) { + try { + // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes. + byte[] eof = new byte[512 * 2]; // newly allocated == zero filled + out.write(eof); + } catch (IOException e) { + Slog.w(TAG, "Error attempting to finalize backup stream"); + } + } + + private void writeAppManifest(PackageInfo pkg, File manifestFile, boolean withApk) + throws IOException { + // Manifest format. All data are strings ending in LF: + // BACKUP_MANIFEST_VERSION, currently 1 + // + // Version 1: + // package name + // package's versionCode + // platform versionCode + // getInstallerPackageName() for this package (maybe empty) + // boolean: "1" if archive includes .apk; any other string means not + // number of signatures == N + // N*: signature byte array in ascii format per Signature.toCharsString() + StringBuilder builder = new StringBuilder(4096); + StringBuilderPrinter printer = new StringBuilderPrinter(builder); + + printer.println(Integer.toString(BACKUP_MANIFEST_VERSION)); + printer.println(pkg.packageName); + printer.println(Integer.toString(pkg.versionCode)); + printer.println(Integer.toString(Build.VERSION.SDK_INT)); + + String installerName = mPackageManager.getInstallerPackageName(pkg.packageName); + printer.println((installerName != null) ? installerName : ""); + + printer.println(withApk ? "1" : "0"); + if (pkg.signatures == null) { + printer.println("0"); + } else { + printer.println(Integer.toString(pkg.signatures.length)); + for (Signature sig : pkg.signatures) { + printer.println(sig.toCharsString()); + } + } + + FileOutputStream outstream = new FileOutputStream(manifestFile); + outstream.write(builder.toString().getBytes()); + outstream.close(); + } + + private void tearDown(PackageInfo pkg) { + if (pkg != null) { + final ApplicationInfo app = pkg.applicationInfo; + if (app != null) { + try { + // unbind and tidy up even on timeout or failure, just in case + mActivityManager.unbindBackupAgent(app); + + // The agent was running with a stub Application object, so shut it down. + if (app.uid != Process.SYSTEM_UID + && app.uid != Process.PHONE_UID) { + if (MORE_DEBUG) Slog.d(TAG, "Backup complete, killing host process"); + mActivityManager.killApplicationProcess(app.processName, app.uid); + } else { + if (MORE_DEBUG) Slog.d(TAG, "Not killing after restore: " + app.processName); + } + } catch (RemoteException e) { + Slog.d(TAG, "Lost app trying to shut down"); + } + } + } + } + + // wrappers for observer use + void sendStartBackup() { + if (mObserver != null) { + try { + mObserver.onStartBackup(); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: startBackup"); + mObserver = null; + } + } + } + + void sendOnBackupPackage(String name) { + if (mObserver != null) { + try { + // TODO: use a more user-friendly name string + mObserver.onBackupPackage(name); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: backupPackage"); + mObserver = null; + } + } + } + + void sendEndBackup() { + if (mObserver != null) { + try { + mObserver.onEndBackup(); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: endBackup"); + mObserver = null; + } + } + } + } + + + // ----- Full restore from a file/socket ----- + + // Description of a file in the restore datastream + static class FileMetadata { + String packageName; // name of the owning app + String installerPackageName; // name of the market-type app that installed the owner + int type; // e.g. BackupAgent.TYPE_DIRECTORY + String domain; // e.g. FullBackup.DATABASE_TREE_TOKEN + String path; // subpath within the semantic domain + long mode; // e.g. 0666 (actually int) + long mtime; // last mod time, UTC time_t (actually int) + long size; // bytes of content + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FileMetadata{"); + sb.append(packageName); sb.append(','); + sb.append(type); sb.append(','); + sb.append(domain); sb.append(':'); sb.append(path); sb.append(','); + sb.append(size); + sb.append('}'); + return sb.toString(); + } + } + + enum RestorePolicy { + IGNORE, + ACCEPT, + ACCEPT_IF_APK + } + + class PerformFullRestoreTask extends ObbServiceClient implements Runnable { + ParcelFileDescriptor mInputFile; + String mCurrentPassword; + String mDecryptPassword; + IFullBackupRestoreObserver mObserver; + AtomicBoolean mLatchObject; + IBackupAgent mAgent; + String mAgentPackage; + ApplicationInfo mTargetApp; + FullBackupObbConnection mObbConnection = null; + ParcelFileDescriptor[] mPipes = null; + + long mBytes; + + // possible handling states for a given package in the restore dataset + final HashMap mPackagePolicies + = new HashMap(); + + // installer package names for each encountered app, derived from the manifests + final HashMap mPackageInstallers = new HashMap(); + + // Signatures for a given package found in its manifest file + final HashMap mManifestSignatures + = new HashMap(); + + // Packages we've already wiped data on when restoring their first file + final HashSet mClearedPackages = new HashSet(); + + PerformFullRestoreTask(ParcelFileDescriptor fd, String curPassword, String decryptPassword, + IFullBackupRestoreObserver observer, AtomicBoolean latch) { + mInputFile = fd; + mCurrentPassword = curPassword; + mDecryptPassword = decryptPassword; + mObserver = observer; + mLatchObject = latch; + mAgent = null; + mAgentPackage = null; + mTargetApp = null; + mObbConnection = new FullBackupObbConnection(); + + // Which packages we've already wiped data on. We prepopulate this + // with a whitelist of packages known to be unclearable. + mClearedPackages.add("android"); + mClearedPackages.add("com.android.providers.settings"); + + } + + class RestoreFileRunnable implements Runnable { + IBackupAgent mAgent; + FileMetadata mInfo; + ParcelFileDescriptor mSocket; + int mToken; + + RestoreFileRunnable(IBackupAgent agent, FileMetadata info, + ParcelFileDescriptor socket, int token) throws IOException { + mAgent = agent; + mInfo = info; + mToken = token; + + // This class is used strictly for process-local binder invocations. The + // semantics of ParcelFileDescriptor differ in this case; in particular, we + // do not automatically get a 'dup'ed descriptor that we can can continue + // to use asynchronously from the caller. So, we make sure to dup it ourselves + // before proceeding to do the restore. + mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); + } + + @Override + public void run() { + try { + mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type, + mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime, + mToken, mBackupManagerBinder); + } catch (RemoteException e) { + // never happens; this is used strictly for local binder calls + } + } + } + + @Override + public void run() { + Slog.i(TAG, "--- Performing full-dataset restore ---"); + mObbConnection.establish(); + sendStartRestore(); + + // Are we able to restore shared-storage data? + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + mPackagePolicies.put(SHARED_BACKUP_AGENT_PACKAGE, RestorePolicy.ACCEPT); + } + + FileInputStream rawInStream = null; + DataInputStream rawDataIn = null; + try { + if (hasBackupPassword()) { + if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; + } + } + + mBytes = 0; + byte[] buffer = new byte[32 * 1024]; + rawInStream = new FileInputStream(mInputFile.getFileDescriptor()); + rawDataIn = new DataInputStream(rawInStream); + + // First, parse out the unencrypted/uncompressed header + boolean compressed = false; + InputStream preCompressStream = rawInStream; + final InputStream in; + + boolean okay = false; + final int headerLen = BACKUP_FILE_HEADER_MAGIC.length(); + byte[] streamHeader = new byte[headerLen]; + rawDataIn.readFully(streamHeader); + byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8"); + if (Arrays.equals(magicBytes, streamHeader)) { + // okay, header looks good. now parse out the rest of the fields. + String s = readHeaderLine(rawInStream); + if (Integer.parseInt(s) == BACKUP_FILE_VERSION) { + // okay, it's a version we recognize + s = readHeaderLine(rawInStream); + compressed = (Integer.parseInt(s) != 0); + s = readHeaderLine(rawInStream); + if (s.equals("none")) { + // no more header to parse; we're good to go + okay = true; + } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { + preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream); + if (preCompressStream != null) { + okay = true; + } + } else Slog.w(TAG, "Archive is encrypted but no password given"); + } else Slog.w(TAG, "Wrong header version: " + s); + } else Slog.w(TAG, "Didn't read the right header magic"); + + if (!okay) { + Slog.w(TAG, "Invalid restore data; aborting."); + return; + } + + // okay, use the right stream layer based on compression + in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream; + + boolean didRestore; + do { + didRestore = restoreOneFile(in, buffer); + } while (didRestore); + + if (MORE_DEBUG) Slog.v(TAG, "Done consuming input tarfile, total bytes=" + mBytes); + } catch (IOException e) { + Slog.e(TAG, "Unable to read restore input"); + } finally { + tearDownPipes(); + tearDownAgent(mTargetApp); + + try { + if (rawDataIn != null) rawDataIn.close(); + if (rawInStream != null) rawInStream.close(); + mInputFile.close(); + } catch (IOException e) { + Slog.w(TAG, "Close of restore data pipe threw", e); + /* nothing we can do about this */ + } + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + synchronized (mLatchObject) { + mLatchObject.set(true); + mLatchObject.notifyAll(); + } + mObbConnection.tearDown(); + sendEndRestore(); + Slog.d(TAG, "Full restore pass complete."); + mWakelock.release(); + } + } + + String readHeaderLine(InputStream in) throws IOException { + int c; + StringBuilder buffer = new StringBuilder(80); + while ((c = in.read()) >= 0) { + if (c == '\n') break; // consume and discard the newlines + buffer.append((char)c); + } + return buffer.toString(); + } + + InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) { + InputStream result = null; + try { + if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { + + String userSaltHex = readHeaderLine(rawInStream); // 5 + byte[] userSalt = hexToByteArray(userSaltHex); + + String ckSaltHex = readHeaderLine(rawInStream); // 6 + byte[] ckSalt = hexToByteArray(ckSaltHex); + + int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7 + String userIvHex = readHeaderLine(rawInStream); // 8 + + String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 + + // decrypt the master key blob + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKey userKey = buildPasswordKey(mDecryptPassword, userSalt, + rounds); + byte[] IV = hexToByteArray(userIvHex); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(userKey.getEncoded(), "AES"), + ivSpec); + byte[] mkCipher = hexToByteArray(masterKeyBlobHex); + byte[] mkBlob = c.doFinal(mkCipher); + + // first, the master key IV + int offset = 0; + int len = mkBlob[offset++]; + IV = Arrays.copyOfRange(mkBlob, offset, offset + len); + offset += len; + // then the master key itself + len = mkBlob[offset++]; + byte[] mk = Arrays.copyOfRange(mkBlob, + offset, offset + len); + offset += len; + // and finally the master key checksum hash + len = mkBlob[offset++]; + byte[] mkChecksum = Arrays.copyOfRange(mkBlob, + offset, offset + len); + + // now validate the decrypted master key against the checksum + byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds); + if (Arrays.equals(calculatedCk, mkChecksum)) { + ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(mk, "AES"), + ivSpec); + // Only if all of the above worked properly will 'result' be assigned + result = new CipherInputStream(rawInStream, c); + } else Slog.w(TAG, "Incorrect password"); + } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName); + } catch (InvalidAlgorithmParameterException e) { + Slog.e(TAG, "Needed parameter spec unavailable!", e); + } catch (BadPaddingException e) { + // This case frequently occurs when the wrong password is used to decrypt + // the master key. Use the identical "incorrect password" log text as is + // used in the checksum failure log in order to avoid providing additional + // information to an attacker. + Slog.w(TAG, "Incorrect password"); + } catch (IllegalBlockSizeException e) { + Slog.w(TAG, "Invalid block size in master key"); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "Needed decryption algorithm unavailable!"); + } catch (NoSuchPaddingException e) { + Slog.e(TAG, "Needed padding mechanism unavailable!"); + } catch (InvalidKeyException e) { + Slog.w(TAG, "Illegal password; aborting"); + } catch (NumberFormatException e) { + Slog.w(TAG, "Can't parse restore data header"); + } catch (IOException e) { + Slog.w(TAG, "Can't read input header"); + } + + return result; + } + + boolean restoreOneFile(InputStream instream, byte[] buffer) { + FileMetadata info; + try { + info = readTarHeaders(instream); + if (info != null) { + if (MORE_DEBUG) { + dumpFileMetadata(info); + } + + final String pkg = info.packageName; + if (!pkg.equals(mAgentPackage)) { + // okay, change in package; set up our various + // bookkeeping if we haven't seen it yet + if (!mPackagePolicies.containsKey(pkg)) { + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + + // Clean up the previous agent relationship if necessary, + // and let the observer know we're considering a new app. + if (mAgent != null) { + if (DEBUG) Slog.d(TAG, "Saw new package; tearing down old one"); + tearDownPipes(); + tearDownAgent(mTargetApp); + mTargetApp = null; + mAgentPackage = null; + } + } + + if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { + mPackagePolicies.put(pkg, readAppManifest(info, instream)); + mPackageInstallers.put(pkg, info.installerPackageName); + // We've read only the manifest content itself at this point, + // so consume the footer before looping around to the next + // input file + skipTarPadding(info.size, instream); + sendOnRestorePackage(pkg); + } else { + // Non-manifest, so it's actual file data. Is this a package + // we're ignoring? + boolean okay = true; + RestorePolicy policy = mPackagePolicies.get(pkg); + switch (policy) { + case IGNORE: + okay = false; + break; + + case ACCEPT_IF_APK: + // If we're in accept-if-apk state, then the first file we + // see MUST be the apk. + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "APK file; installing"); + // Try to install the app. + String installerName = mPackageInstallers.get(pkg); + okay = installApk(info, installerName, instream); + // good to go; promote to ACCEPT + mPackagePolicies.put(pkg, (okay) + ? RestorePolicy.ACCEPT + : RestorePolicy.IGNORE); + // At this point we've consumed this file entry + // ourselves, so just strip the tar footer and + // go on to the next file in the input stream + skipTarPadding(info.size, instream); + return true; + } else { + // File data before (or without) the apk. We can't + // handle it coherently in this case so ignore it. + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + okay = false; + } + break; + + case ACCEPT: + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "apk present but ACCEPT"); + // we can take the data without the apk, so we + // *want* to do so. skip the apk by declaring this + // one file not-okay without changing the restore + // policy for the package. + okay = false; + } + break; + + default: + // Something has gone dreadfully wrong when determining + // the restore policy from the manifest. Ignore the + // rest of this package's data. + Slog.e(TAG, "Invalid policy from manifest"); + okay = false; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + break; + } + + // If the policy is satisfied, go ahead and set up to pipe the + // data to the agent. + if (DEBUG && okay && mAgent != null) { + Slog.i(TAG, "Reusing existing agent instance"); + } + if (okay && mAgent == null) { + if (DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg); + + try { + mTargetApp = mPackageManager.getApplicationInfo(pkg, 0); + + // If we haven't sent any data to this app yet, we probably + // need to clear it first. Check that. + if (!mClearedPackages.contains(pkg)) { + // apps with their own backup agents are + // responsible for coherently managing a full + // restore. + if (mTargetApp.backupAgentName == null) { + if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore"); + clearApplicationDataSynchronous(pkg); + } else { + if (DEBUG) Slog.d(TAG, "backup agent (" + + mTargetApp.backupAgentName + ") => no clear"); + } + mClearedPackages.add(pkg); + } else { + if (DEBUG) Slog.d(TAG, "We've initialized this app already; no clear required"); + } + + // All set; now set up the IPC and launch the agent + setUpPipes(); + mAgent = bindToAgentSynchronous(mTargetApp, + IApplicationThread.BACKUP_MODE_RESTORE_FULL); + mAgentPackage = pkg; + } catch (IOException e) { + // fall through to error handling + } catch (NameNotFoundException e) { + // fall through to error handling + } + + if (mAgent == null) { + if (DEBUG) Slog.d(TAG, "Unable to create agent for " + pkg); + okay = false; + tearDownPipes(); + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Sanity check: make sure we never give data to the wrong app. This + // should never happen but a little paranoia here won't go amiss. + if (okay && !pkg.equals(mAgentPackage)) { + Slog.e(TAG, "Restoring data for " + pkg + + " but agent is for " + mAgentPackage); + okay = false; + } + + // At this point we have an agent ready to handle the full + // restore data as well as a pipe for sending data to + // that agent. Tell the agent to start reading from the + // pipe. + if (okay) { + boolean agentSuccess = true; + long toCopy = info.size; + final int token = generateToken(); + try { + prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); + if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg + + " : " + info.path); + mObbConnection.restoreObbFile(pkg, mPipes[0], + info.size, info.type, info.path, info.mode, + info.mtime, token, mBackupManagerBinder); + } else { + if (DEBUG) Slog.d(TAG, "Invoking agent to restore file " + + info.path); + // fire up the app's agent listening on the socket. If + // the agent is running in the system process we can't + // just invoke it asynchronously, so we provide a thread + // for it here. + if (mTargetApp.processName.equals("system")) { + Slog.d(TAG, "system process agent - spinning a thread"); + RestoreFileRunnable runner = new RestoreFileRunnable( + mAgent, info, mPipes[0], token); + new Thread(runner).start(); + } else { + mAgent.doRestoreFile(mPipes[0], info.size, info.type, + info.domain, info.path, info.mode, info.mtime, + token, mBackupManagerBinder); + } + } + } catch (IOException e) { + // couldn't dup the socket for a process-local restore + Slog.d(TAG, "Couldn't establish restore"); + agentSuccess = false; + okay = false; + } catch (RemoteException e) { + // whoops, remote entity went away. We'll eat the content + // ourselves, then, and not copy it over. + Slog.e(TAG, "Agent crashed during full restore"); + agentSuccess = false; + okay = false; + } + + // Copy over the data if the agent is still good + if (okay) { + boolean pipeOkay = true; + FileOutputStream pipe = new FileOutputStream( + mPipes[1].getFileDescriptor()); + while (toCopy > 0) { + int toRead = (toCopy > buffer.length) + ? buffer.length : (int)toCopy; + int nRead = instream.read(buffer, 0, toRead); + if (nRead >= 0) mBytes += nRead; + if (nRead <= 0) break; + toCopy -= nRead; + + // send it to the output pipe as long as things + // are still good + if (pipeOkay) { + try { + pipe.write(buffer, 0, nRead); + } catch (IOException e) { + Slog.e(TAG, "Failed to write to restore pipe", e); + pipeOkay = false; + } + } + } + + // done sending that file! Now we just need to consume + // the delta from info.size to the end of block. + skipTarPadding(info.size, instream); + + // and now that we've sent it all, wait for the remote + // side to acknowledge receipt + agentSuccess = waitUntilOperationComplete(token); + } + + // okay, if the remote end failed at any point, deal with + // it by ignoring the rest of the restore on it + if (!agentSuccess) { + mBackupHandler.removeMessages(MSG_TIMEOUT); + tearDownPipes(); + tearDownAgent(mTargetApp); + mAgent = null; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Problems setting up the agent communication, or an already- + // ignored package: skip to the next tar stream entry by + // reading and discarding this file. + if (!okay) { + if (DEBUG) Slog.d(TAG, "[discarding file content]"); + long bytesToConsume = (info.size + 511) & ~511; + while (bytesToConsume > 0) { + int toRead = (bytesToConsume > buffer.length) + ? buffer.length : (int)bytesToConsume; + long nRead = instream.read(buffer, 0, toRead); + if (nRead >= 0) mBytes += nRead; + if (nRead <= 0) break; + bytesToConsume -= nRead; + } + } + } + } + } catch (IOException e) { + if (DEBUG) Slog.w(TAG, "io exception on restore socket read", e); + // treat as EOF + info = null; + } + + return (info != null); + } + + void setUpPipes() throws IOException { + mPipes = ParcelFileDescriptor.createPipe(); + } + + void tearDownPipes() { + if (mPipes != null) { + try { + mPipes[0].close(); + mPipes[0] = null; + mPipes[1].close(); + mPipes[1] = null; + } catch (IOException e) { + Slog.w(TAG, "Couldn't close agent pipes", e); + } + mPipes = null; + } + } + + void tearDownAgent(ApplicationInfo app) { + if (mAgent != null) { + try { + // unbind and tidy up even on timeout or failure, just in case + mActivityManager.unbindBackupAgent(app); + + // The agent was running with a stub Application object, so shut it down. + // !!! We hardcode the confirmation UI's package name here rather than use a + // manifest flag! TODO something less direct. + if (app.uid != Process.SYSTEM_UID + && !app.packageName.equals("com.android.backupconfirm")) { + if (DEBUG) Slog.d(TAG, "Killing host process"); + mActivityManager.killApplicationProcess(app.processName, app.uid); + } else { + if (DEBUG) Slog.d(TAG, "Not killing after full restore"); + } + } catch (RemoteException e) { + Slog.d(TAG, "Lost app trying to shut down"); + } + mAgent = null; + } + } + + class RestoreInstallObserver extends IPackageInstallObserver.Stub { + final AtomicBoolean mDone = new AtomicBoolean(); + String mPackageName; + int mResult; + + public void reset() { + synchronized (mDone) { + mDone.set(false); + } + } + + public void waitForCompletion() { + synchronized (mDone) { + while (mDone.get() == false) { + try { + mDone.wait(); + } catch (InterruptedException e) { } + } + } + } + + int getResult() { + return mResult; + } + + @Override + public void packageInstalled(String packageName, int returnCode) + throws RemoteException { + synchronized (mDone) { + mResult = returnCode; + mPackageName = packageName; + mDone.set(true); + mDone.notifyAll(); + } + } + } + + class RestoreDeleteObserver extends IPackageDeleteObserver.Stub { + final AtomicBoolean mDone = new AtomicBoolean(); + int mResult; + + public void reset() { + synchronized (mDone) { + mDone.set(false); + } + } + + public void waitForCompletion() { + synchronized (mDone) { + while (mDone.get() == false) { + try { + mDone.wait(); + } catch (InterruptedException e) { } + } + } + } + + @Override + public void packageDeleted(String packageName, int returnCode) throws RemoteException { + synchronized (mDone) { + mResult = returnCode; + mDone.set(true); + mDone.notifyAll(); + } + } + } + + final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); + final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); + + boolean installApk(FileMetadata info, String installerPackage, InputStream instream) { + boolean okay = true; + + if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName); + + // The file content is an .apk file. Copy it out to a staging location and + // attempt to install it. + File apkFile = new File(mDataDir, info.packageName); + try { + FileOutputStream apkStream = new FileOutputStream(apkFile); + byte[] buffer = new byte[32 * 1024]; + long size = info.size; + while (size > 0) { + long toRead = (buffer.length < size) ? buffer.length : size; + int didRead = instream.read(buffer, 0, (int)toRead); + if (didRead >= 0) mBytes += didRead; + apkStream.write(buffer, 0, didRead); + size -= didRead; + } + apkStream.close(); + + // make sure the installer can read it + apkFile.setReadable(true, false); + + // Now install it + Uri packageUri = Uri.fromFile(apkFile); + mInstallObserver.reset(); + mPackageManager.installPackage(packageUri, mInstallObserver, + PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB, + installerPackage); + mInstallObserver.waitForCompletion(); + + if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) { + // The only time we continue to accept install of data even if the + // apk install failed is if we had already determined that we could + // accept the data regardless. + if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) { + okay = false; + } + } else { + // Okay, the install succeeded. Make sure it was the right app. + boolean uninstall = false; + if (!mInstallObserver.mPackageName.equals(info.packageName)) { + Slog.w(TAG, "Restore stream claimed to include apk for " + + info.packageName + " but apk was really " + + mInstallObserver.mPackageName); + // delete the package we just put in place; it might be fraudulent + okay = false; + uninstall = true; + } else { + try { + PackageInfo pkg = mPackageManager.getPackageInfo(info.packageName, + PackageManager.GET_SIGNATURES); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) { + Slog.w(TAG, "Restore stream contains apk of package " + + info.packageName + " but it disallows backup/restore"); + okay = false; + } else { + // So far so good -- do the signatures match the manifest? + Signature[] sigs = mManifestSignatures.get(info.packageName); + if (signaturesMatch(sigs, pkg)) { + // If this is a system-uid app without a declared backup agent, + // don't restore any of the file data. + if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) + && (pkg.applicationInfo.backupAgentName == null)) { + Slog.w(TAG, "Installed app " + info.packageName + + " has restricted uid and no agent"); + okay = false; + } + } else { + Slog.w(TAG, "Installed app " + info.packageName + + " signatures do not match restore manifest"); + okay = false; + uninstall = true; + } + } + } catch (NameNotFoundException e) { + Slog.w(TAG, "Install of package " + info.packageName + + " succeeded but now not found"); + okay = false; + } + } + + // If we're not okay at this point, we need to delete the package + // that we just installed. + if (uninstall) { + mDeleteObserver.reset(); + mPackageManager.deletePackage(mInstallObserver.mPackageName, + mDeleteObserver, 0); + mDeleteObserver.waitForCompletion(); + } + } + } catch (IOException e) { + Slog.e(TAG, "Unable to transcribe restored apk for install"); + okay = false; + } finally { + apkFile.delete(); + } + + return okay; + } + + // Given an actual file content size, consume the post-content padding mandated + // by the tar format. + void skipTarPadding(long size, InputStream instream) throws IOException { + long partial = (size + 512) % 512; + if (partial > 0) { + final int needed = 512 - (int)partial; + byte[] buffer = new byte[needed]; + if (readExactly(instream, buffer, 0, needed) == needed) { + mBytes += needed; + } else throw new IOException("Unexpected EOF in padding"); + } + } + + // Returns a policy constant; takes a buffer arg to reduce memory churn + RestorePolicy readAppManifest(FileMetadata info, InputStream instream) + throws IOException { + // Fail on suspiciously large manifest files + if (info.size > 64 * 1024) { + throw new IOException("Restore manifest too big; corrupt? size=" + info.size); + } + + byte[] buffer = new byte[(int) info.size]; + if (readExactly(instream, buffer, 0, (int)info.size) == info.size) { + mBytes += info.size; + } else throw new IOException("Unexpected EOF in manifest"); + + RestorePolicy policy = RestorePolicy.IGNORE; + String[] str = new String[1]; + int offset = 0; + + try { + offset = extractLine(buffer, offset, str); + int version = Integer.parseInt(str[0]); + if (version == BACKUP_MANIFEST_VERSION) { + offset = extractLine(buffer, offset, str); + String manifestPackage = str[0]; + // TODO: handle + if (manifestPackage.equals(info.packageName)) { + offset = extractLine(buffer, offset, str); + version = Integer.parseInt(str[0]); // app version + offset = extractLine(buffer, offset, str); + int platformVersion = Integer.parseInt(str[0]); + offset = extractLine(buffer, offset, str); + info.installerPackageName = (str[0].length() > 0) ? str[0] : null; + offset = extractLine(buffer, offset, str); + boolean hasApk = str[0].equals("1"); + offset = extractLine(buffer, offset, str); + int numSigs = Integer.parseInt(str[0]); + if (numSigs > 0) { + Signature[] sigs = new Signature[numSigs]; + for (int i = 0; i < numSigs; i++) { + offset = extractLine(buffer, offset, str); + sigs[i] = new Signature(str[0]); + } + mManifestSignatures.put(info.packageName, sigs); + + // Okay, got the manifest info we need... + try { + PackageInfo pkgInfo = mPackageManager.getPackageInfo( + info.packageName, PackageManager.GET_SIGNATURES); + // Fall through to IGNORE if the app explicitly disallows backup + final int flags = pkgInfo.applicationInfo.flags; + if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) { + // Restore system-uid-space packages only if they have + // defined a custom backup agent + if ((pkgInfo.applicationInfo.uid >= Process.FIRST_APPLICATION_UID) + || (pkgInfo.applicationInfo.backupAgentName != null)) { + // Verify signatures against any installed version; if they + // don't match, then we fall though and ignore the data. The + // signatureMatch() method explicitly ignores the signature + // check for packages installed on the system partition, because + // such packages are signed with the platform cert instead of + // the app developer's cert, so they're different on every + // device. + if (signaturesMatch(sigs, pkgInfo)) { + if (pkgInfo.versionCode >= version) { + Slog.i(TAG, "Sig + version match; taking data"); + policy = RestorePolicy.ACCEPT; + } else { + // The data is from a newer version of the app than + // is presently installed. That means we can only + // use it if the matching apk is also supplied. + Slog.d(TAG, "Data version " + version + + " is newer than installed version " + + pkgInfo.versionCode + " - requiring apk"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + } else { + Slog.w(TAG, "Restore manifest signatures do not match " + + "installed application for " + info.packageName); + } + } else { + Slog.w(TAG, "Package " + info.packageName + + " is system level with no agent"); + } + } else { + if (DEBUG) Slog.i(TAG, "Restore manifest from " + + info.packageName + " but allowBackup=false"); + } + } catch (NameNotFoundException e) { + // Okay, the target app isn't installed. We can process + // the restore properly only if the dataset provides the + // apk file and we can successfully install it. + if (DEBUG) Slog.i(TAG, "Package " + info.packageName + + " not installed; requiring apk in dataset"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + + if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) { + Slog.i(TAG, "Cannot restore package " + info.packageName + + " without the matching .apk"); + } + } else { + Slog.i(TAG, "Missing signature on backed-up package " + + info.packageName); + } + } else { + Slog.i(TAG, "Expected package " + info.packageName + + " but restore manifest claims " + manifestPackage); + } + } else { + Slog.i(TAG, "Unknown restore manifest version " + version + + " for package " + info.packageName); + } + } catch (NumberFormatException e) { + Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); + } catch (IllegalArgumentException e) { + Slog.w(TAG, e.getMessage()); + } + + return policy; + } + + // Builds a line from a byte buffer starting at 'offset', and returns + // the index of the next unconsumed data in the buffer. + int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException { + final int end = buffer.length; + if (offset >= end) throw new IOException("Incomplete data"); + + int pos; + for (pos = offset; pos < end; pos++) { + byte c = buffer[pos]; + // at LF we declare end of line, and return the next char as the + // starting point for the next time through + if (c == '\n') { + break; + } + } + outStr[0] = new String(buffer, offset, pos - offset); + pos++; // may be pointing an extra byte past the end but that's okay + return pos; + } + + void dumpFileMetadata(FileMetadata info) { + if (DEBUG) { + StringBuilder b = new StringBuilder(128); + + // mode string + b.append((info.type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-'); + b.append(((info.mode & 0400) != 0) ? 'r' : '-'); + b.append(((info.mode & 0200) != 0) ? 'w' : '-'); + b.append(((info.mode & 0100) != 0) ? 'x' : '-'); + b.append(((info.mode & 0040) != 0) ? 'r' : '-'); + b.append(((info.mode & 0020) != 0) ? 'w' : '-'); + b.append(((info.mode & 0010) != 0) ? 'x' : '-'); + b.append(((info.mode & 0004) != 0) ? 'r' : '-'); + b.append(((info.mode & 0002) != 0) ? 'w' : '-'); + b.append(((info.mode & 0001) != 0) ? 'x' : '-'); + b.append(String.format(" %9d ", info.size)); + + Date stamp = new Date(info.mtime); + b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp)); + + b.append(info.packageName); + b.append(" :: "); + b.append(info.domain); + b.append(" :: "); + b.append(info.path); + + Slog.i(TAG, b.toString()); + } + } + // Consume a tar file header block [sequence] and accumulate the relevant metadata + FileMetadata readTarHeaders(InputStream instream) throws IOException { + byte[] block = new byte[512]; + FileMetadata info = null; + + boolean gotHeader = readTarHeader(instream, block); + if (gotHeader) { + try { + // okay, presume we're okay, and extract the various metadata + info = new FileMetadata(); + info.size = extractRadix(block, 124, 12, 8); + info.mtime = extractRadix(block, 136, 12, 8); + info.mode = extractRadix(block, 100, 8, 8); + + info.path = extractString(block, 345, 155); // prefix + String path = extractString(block, 0, 100); + if (path.length() > 0) { + if (info.path.length() > 0) info.path += '/'; + info.path += path; + } + + // tar link indicator field: 1 byte at offset 156 in the header. + int typeChar = block[156]; + if (typeChar == 'x') { + // pax extended header, so we need to read that + gotHeader = readPaxExtendedHeader(instream, info); + if (gotHeader) { + // and after a pax extended header comes another real header -- read + // that to find the real file type + gotHeader = readTarHeader(instream, block); + } + if (!gotHeader) throw new IOException("Bad or missing pax header"); + + typeChar = block[156]; + } + + switch (typeChar) { + case '0': info.type = BackupAgent.TYPE_FILE; break; + case '5': { + info.type = BackupAgent.TYPE_DIRECTORY; + if (info.size != 0) { + Slog.w(TAG, "Directory entry with nonzero size in header"); + info.size = 0; + } + break; + } + case 0: { + // presume EOF + if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info); + return null; + } + default: { + Slog.e(TAG, "Unknown tar entity type: " + typeChar); + throw new IOException("Unknown entity type " + typeChar); + } + } + + // Parse out the path + // + // first: apps/shared/unrecognized + if (FullBackup.SHARED_PREFIX.regionMatches(0, + info.path, 0, FullBackup.SHARED_PREFIX.length())) { + // File in shared storage. !!! TODO: implement this. + info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); + info.packageName = SHARED_BACKUP_AGENT_PACKAGE; + info.domain = FullBackup.SHARED_STORAGE_TOKEN; + if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path); + } else if (FullBackup.APPS_PREFIX.regionMatches(0, + info.path, 0, FullBackup.APPS_PREFIX.length())) { + // App content! Parse out the package name and domain + + // strip the apps/ prefix + info.path = info.path.substring(FullBackup.APPS_PREFIX.length()); + + // extract the package name + int slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in " + info.path); + info.packageName = info.path.substring(0, slash); + info.path = info.path.substring(slash+1); + + // if it's a manifest we're done, otherwise parse out the domains + if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) { + slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path); + info.domain = info.path.substring(0, slash); + info.path = info.path.substring(slash + 1); + } + } + } catch (IOException e) { + if (DEBUG) { + Slog.e(TAG, "Parse error in header: " + e.getMessage()); + HEXLOG(block); + } + throw e; + } + } + return info; + } + + private void HEXLOG(byte[] block) { + int offset = 0; + int todo = block.length; + StringBuilder buf = new StringBuilder(64); + while (todo > 0) { + buf.append(String.format("%04x ", offset)); + int numThisLine = (todo > 16) ? 16 : todo; + for (int i = 0; i < numThisLine; i++) { + buf.append(String.format("%02x ", block[offset+i])); + } + Slog.i("hexdump", buf.toString()); + buf.setLength(0); + todo -= numThisLine; + offset += numThisLine; + } + } + + // Read exactly the given number of bytes into a buffer at the stated offset. + // Returns false if EOF is encountered before the requested number of bytes + // could be read. + int readExactly(InputStream in, byte[] buffer, int offset, int size) + throws IOException { + if (size <= 0) throw new IllegalArgumentException("size must be > 0"); + + int soFar = 0; + while (soFar < size) { + int nRead = in.read(buffer, offset + soFar, size - soFar); + if (nRead <= 0) { + if (MORE_DEBUG) Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar); + break; + } + soFar += nRead; + } + return soFar; + } + + boolean readTarHeader(InputStream instream, byte[] block) throws IOException { + final int got = readExactly(instream, block, 0, 512); + if (got == 0) return false; // Clean EOF + if (got < 512) throw new IOException("Unable to read full block header"); + mBytes += 512; + return true; + } + + // overwrites 'info' fields based on the pax extended header + boolean readPaxExtendedHeader(InputStream instream, FileMetadata info) + throws IOException { + // We should never see a pax extended header larger than this + if (info.size > 32*1024) { + Slog.w(TAG, "Suspiciously large pax header size " + info.size + + " - aborting"); + throw new IOException("Sanity failure: pax header size " + info.size); + } + + // read whole blocks, not just the content size + int numBlocks = (int)((info.size + 511) >> 9); + byte[] data = new byte[numBlocks * 512]; + if (readExactly(instream, data, 0, data.length) < data.length) { + throw new IOException("Unable to read full pax header"); + } + mBytes += data.length; + + final int contentSize = (int) info.size; + int offset = 0; + do { + // extract the line at 'offset' + int eol = offset+1; + while (eol < contentSize && data[eol] != ' ') eol++; + if (eol >= contentSize) { + // error: we just hit EOD looking for the end of the size field + throw new IOException("Invalid pax data"); + } + // eol points to the space between the count and the key + int linelen = (int) extractRadix(data, offset, eol - offset, 10); + int key = eol + 1; // start of key=value + eol = offset + linelen - 1; // trailing LF + int value; + for (value = key+1; data[value] != '=' && value <= eol; value++); + if (value > eol) { + throw new IOException("Invalid pax declaration"); + } + + // pax requires that key/value strings be in UTF-8 + String keyStr = new String(data, key, value-key, "UTF-8"); + // -1 to strip the trailing LF + String valStr = new String(data, value+1, eol-value-1, "UTF-8"); + + if ("path".equals(keyStr)) { + info.path = valStr; + } else if ("size".equals(keyStr)) { + info.size = Long.parseLong(valStr); + } else { + if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key); + } + + offset += linelen; + } while (offset < contentSize); + + return true; + } + + long extractRadix(byte[] data, int offset, int maxChars, int radix) + throws IOException { + long value = 0; + final int end = offset + maxChars; + for (int i = offset; i < end; i++) { + final byte b = data[i]; + // Numeric fields in tar can terminate with either NUL or SPC + if (b == 0 || b == ' ') break; + if (b < '0' || b > ('0' + radix - 1)) { + throw new IOException("Invalid number in header: '" + (char)b + "' for radix " + radix); + } + value = radix * value + (b - '0'); + } + return value; + } + + String extractString(byte[] data, int offset, int maxChars) throws IOException { + final int end = offset + maxChars; + int eos = offset; + // tar string fields terminate early with a NUL + while (eos < end && data[eos] != 0) eos++; + return new String(data, offset, eos-offset, "US-ASCII"); + } + + void sendStartRestore() { + if (mObserver != null) { + try { + mObserver.onStartRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: startRestore"); + mObserver = null; + } + } + } + + void sendOnRestorePackage(String name) { + if (mObserver != null) { + try { + // TODO: use a more user-friendly name string + mObserver.onRestorePackage(name); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: restorePackage"); + mObserver = null; + } + } + } + + void sendEndRestore() { + if (mObserver != null) { + try { + mObserver.onEndRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: endRestore"); + mObserver = null; + } + } + } + } + + // ----- Restore handling ----- + + private boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) { + // If the target resides on the system partition, we allow it to restore + // data from the like-named package in a restore set even if the signatures + // do not match. (Unlike general applications, those flashed to the system + // partition will be signed with the device's platform certificate, so on + // different phones the same system app will have different signatures.) + if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + if (DEBUG) Slog.v(TAG, "System app " + target.packageName + " - skipping sig check"); + return true; + } + + // Allow unsigned apps, but not signed on one device and unsigned on the other + // !!! TODO: is this the right policy? + Signature[] deviceSigs = target.signatures; + if (MORE_DEBUG) Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + + " device=" + deviceSigs); + if ((storedSigs == null || storedSigs.length == 0) + && (deviceSigs == null || deviceSigs.length == 0)) { + return true; + } + if (storedSigs == null || deviceSigs == null) { + return false; + } + + // !!! TODO: this demands that every stored signature match one + // that is present on device, and does not demand the converse. + // Is this this right policy? + int nStored = storedSigs.length; + int nDevice = deviceSigs.length; + + for (int i=0; i < nStored; i++) { + boolean match = false; + for (int j=0; j < nDevice; j++) { + if (storedSigs[i].equals(deviceSigs[j])) { + match = true; + break; + } + } + if (!match) { + return false; + } + } + return true; + } + + enum RestoreState { + INITIAL, + DOWNLOAD_DATA, + PM_METADATA, + RUNNING_QUEUE, + FINAL + } + + class PerformRestoreTask implements BackupRestoreTask { + private IBackupTransport mTransport; + private IRestoreObserver mObserver; + private long mToken; + private PackageInfo mTargetPackage; + private File mStateDir; + private int mPmToken; + private boolean mNeedFullBackup; + private HashSet mFilterSet; + private long mStartRealtime; + private PackageManagerBackupAgent mPmAgent; + private List mAgentPackages; + private ArrayList mRestorePackages; + private RestoreState mCurrentState; + private int mCount; + private boolean mFinished; + private int mStatus; + private File mBackupDataName; + private File mNewStateName; + private File mSavedStateName; + private ParcelFileDescriptor mBackupData; + private ParcelFileDescriptor mNewState; + private PackageInfo mCurrentPackage; + + + class RestoreRequest { + public PackageInfo app; + public int storedAppVersion; + + RestoreRequest(PackageInfo _app, int _version) { + app = _app; + storedAppVersion = _version; + } + } + + PerformRestoreTask(IBackupTransport transport, String dirName, IRestoreObserver observer, + long restoreSetToken, PackageInfo targetPackage, int pmToken, + boolean needFullBackup, String[] filterSet) { + mCurrentState = RestoreState.INITIAL; + mFinished = false; + mPmAgent = null; + + mTransport = transport; + mObserver = observer; + mToken = restoreSetToken; + mTargetPackage = targetPackage; + mPmToken = pmToken; + mNeedFullBackup = needFullBackup; + + if (filterSet != null) { + mFilterSet = new HashSet(); + for (String pkg : filterSet) { + mFilterSet.add(pkg); + } + } else { + mFilterSet = null; + } + + mStateDir = new File(mBaseStateDir, dirName); + } + + // Execute one tick of whatever state machine the task implements + @Override + public void execute() { + if (MORE_DEBUG) Slog.v(TAG, "*** Executing restore step: " + mCurrentState); + switch (mCurrentState) { + case INITIAL: + beginRestore(); + break; + + case DOWNLOAD_DATA: + downloadRestoreData(); + break; + + case PM_METADATA: + restorePmMetadata(); + break; + + case RUNNING_QUEUE: + restoreNextAgent(); + break; + + case FINAL: + if (!mFinished) finalizeRestore(); + else { + Slog.e(TAG, "Duplicate finish"); + } + mFinished = true; + break; + } + } + + // Initialize and set up for the PM metadata restore, which comes first + void beginRestore() { + // Don't account time doing the restore as inactivity of the app + // that has opened a restore session. + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + + // Assume error until we successfully init everything + mStatus = BackupConstants.TRANSPORT_ERROR; + + try { + // TODO: Log this before getAvailableRestoreSets, somehow + EventLog.writeEvent(EventLogTags.RESTORE_START, mTransport.transportDirName(), mToken); + + // Get the list of all packages which have backup enabled. + // (Include the Package Manager metadata pseudo-package first.) + mRestorePackages = new ArrayList(); + PackageInfo omPackage = new PackageInfo(); + omPackage.packageName = PACKAGE_MANAGER_SENTINEL; + mRestorePackages.add(omPackage); + + mAgentPackages = allAgentPackages(); + if (mTargetPackage == null) { + // if there's a filter set, strip out anything that isn't + // present before proceeding + if (mFilterSet != null) { + for (int i = mAgentPackages.size() - 1; i >= 0; i--) { + final PackageInfo pkg = mAgentPackages.get(i); + if (! mFilterSet.contains(pkg.packageName)) { + mAgentPackages.remove(i); + } + } + if (MORE_DEBUG) { + Slog.i(TAG, "Post-filter package set for restore:"); + for (PackageInfo p : mAgentPackages) { + Slog.i(TAG, " " + p); + } + } + } + mRestorePackages.addAll(mAgentPackages); + } else { + // Just one package to attempt restore of + mRestorePackages.add(mTargetPackage); + } + + // let the observer know that we're running + if (mObserver != null) { + try { + // !!! TODO: get an actual count from the transport after + // its startRestore() runs? + mObserver.restoreStarting(mRestorePackages.size()); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died at restoreStarting"); + mObserver = null; + } + } + } catch (RemoteException e) { + // Something has gone catastrophically wrong with the transport + Slog.e(TAG, "Error communicating with transport for restore"); + executeNextState(RestoreState.FINAL); + return; + } + + mStatus = BackupConstants.TRANSPORT_OK; + executeNextState(RestoreState.DOWNLOAD_DATA); + } + + void downloadRestoreData() { + // Note that the download phase can be very time consuming, but we're executing + // it inline here on the looper. This is "okay" because it is not calling out to + // third party code; the transport is "trusted," and so we assume it is being a + // good citizen and timing out etc when appropriate. + // + // TODO: when appropriate, move the download off the looper and rearrange the + // error handling around that. + try { + mStatus = mTransport.startRestore(mToken, + mRestorePackages.toArray(new PackageInfo[0])); + if (mStatus != BackupConstants.TRANSPORT_OK) { + Slog.e(TAG, "Error starting restore operation"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + executeNextState(RestoreState.FINAL); + return; + } + } catch (RemoteException e) { + Slog.e(TAG, "Error communicating with transport for restore"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + return; + } + + // Successful download of the data to be parceled out to the apps, so off we go. + executeNextState(RestoreState.PM_METADATA); + } + + void restorePmMetadata() { + try { + String packageName = mTransport.nextRestorePackage(); + if (packageName == null) { + Slog.e(TAG, "Error getting first restore package"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + return; + } else if (packageName.equals("")) { + Slog.i(TAG, "No restore data available"); + int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); + EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, 0, millis); + mStatus = BackupConstants.TRANSPORT_OK; + executeNextState(RestoreState.FINAL); + return; + } else if (!packageName.equals(PACKAGE_MANAGER_SENTINEL)) { + Slog.e(TAG, "Expected restore data for \"" + PACKAGE_MANAGER_SENTINEL + + "\", found only \"" + packageName + "\""); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, + "Package manager data missing"); + executeNextState(RestoreState.FINAL); + return; + } + + // Pull the Package Manager metadata from the restore set first + PackageInfo omPackage = new PackageInfo(); + omPackage.packageName = PACKAGE_MANAGER_SENTINEL; + mPmAgent = new PackageManagerBackupAgent( + mPackageManager, mAgentPackages); + initiateOneRestore(omPackage, 0, IBackupAgent.Stub.asInterface(mPmAgent.onBind()), + mNeedFullBackup); + // The PM agent called operationComplete() already, because our invocation + // of it is process-local and therefore synchronous. That means that a + // RUNNING_QUEUE message is already enqueued. Only if we're unable to + // proceed with running the queue do we remove that pending message and + // jump straight to the FINAL state. + + // Verify that the backup set includes metadata. If not, we can't do + // signature/version verification etc, so we simply do not proceed with + // the restore operation. + if (!mPmAgent.hasMetadata()) { + Slog.e(TAG, "No restore metadata available, so not restoring settings"); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, + "Package manager restore metadata missing"); + mStatus = BackupConstants.TRANSPORT_ERROR; + mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); + executeNextState(RestoreState.FINAL); + return; + } + } catch (RemoteException e) { + Slog.e(TAG, "Error communicating with transport for restore"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); + executeNextState(RestoreState.FINAL); + return; + } + + // Metadata is intact, so we can now run the restore queue. If we get here, + // we have already enqueued the necessary next-step message on the looper. + } + + void restoreNextAgent() { + try { + String packageName = mTransport.nextRestorePackage(); + + if (packageName == null) { + Slog.e(TAG, "Error getting next restore package"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + executeNextState(RestoreState.FINAL); + return; + } else if (packageName.equals("")) { + if (DEBUG) Slog.v(TAG, "No next package, finishing restore"); + int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); + EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis); + executeNextState(RestoreState.FINAL); + return; + } + + if (mObserver != null) { + try { + mObserver.onUpdate(mCount, packageName); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died in onUpdate"); + mObserver = null; + } + } + + Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName); + if (metaInfo == null) { + Slog.e(TAG, "Missing metadata for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package metadata missing"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + PackageInfo packageInfo; + try { + int flags = PackageManager.GET_SIGNATURES; + packageInfo = mPackageManager.getPackageInfo(packageName, flags); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Invalid package restoring data", e); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package missing on device"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (packageInfo.applicationInfo.backupAgentName == null + || "".equals(packageInfo.applicationInfo.backupAgentName)) { + if (DEBUG) { + Slog.i(TAG, "Data exists for package " + packageName + + " but app has no agent; skipping"); + } + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package has no agent"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (metaInfo.versionCode > packageInfo.versionCode) { + // Data is from a "newer" version of the app than we have currently + // installed. If the app has not declared that it is prepared to + // handle this case, we do not attempt the restore. + if ((packageInfo.applicationInfo.flags + & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) { + String message = "Version " + metaInfo.versionCode + + " > installed version " + packageInfo.versionCode; + Slog.w(TAG, "Package " + packageName + ": " + message); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, + packageName, message); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } else { + if (DEBUG) Slog.v(TAG, "Version " + metaInfo.versionCode + + " > installed " + packageInfo.versionCode + + " but restoreAnyVersion"); + } + } + + if (!signaturesMatch(metaInfo.signatures, packageInfo)) { + Slog.w(TAG, "Signature mismatch restoring " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Signature mismatch"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (DEBUG) Slog.v(TAG, "Package " + packageName + + " restore version [" + metaInfo.versionCode + + "] is compatible with installed version [" + + packageInfo.versionCode + "]"); + + // Then set up and bind the agent + IBackupAgent agent = bindToAgentSynchronous( + packageInfo.applicationInfo, + IApplicationThread.BACKUP_MODE_INCREMENTAL); + if (agent == null) { + Slog.w(TAG, "Can't find backup agent for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Restore agent missing"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + // And then finally start the restore on this agent + try { + initiateOneRestore(packageInfo, metaInfo.versionCode, agent, mNeedFullBackup); + ++mCount; + } catch (Exception e) { + Slog.e(TAG, "Error when attempting restore: " + e.toString()); + agentErrorCleanup(); + executeNextState(RestoreState.RUNNING_QUEUE); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to fetch restore data from transport"); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + } + } + + void finalizeRestore() { + if (MORE_DEBUG) Slog.d(TAG, "finishing restore mObserver=" + mObserver); + + try { + mTransport.finishRestore(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing restore", e); + } + + if (mObserver != null) { + try { + mObserver.restoreFinished(mStatus); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died at restoreFinished"); + } + } + + // If this was a restoreAll operation, record that this was our + // ancestral dataset, as well as the set of apps that are possibly + // restoreable from the dataset + if (mTargetPackage == null && mPmAgent != null) { + mAncestralPackages = mPmAgent.getRestoredPackages(); + mAncestralToken = mToken; + writeRestoreTokens(); + } + + // We must under all circumstances tell the Package Manager to + // proceed with install notifications if it's waiting for us. + if (mPmToken > 0) { + if (MORE_DEBUG) Slog.v(TAG, "finishing PM token " + mPmToken); + try { + mPackageManagerBinder.finishPackageInstall(mPmToken); + } catch (RemoteException e) { /* can't happen */ } + } + + // Furthermore we need to reset the session timeout clock + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, + TIMEOUT_RESTORE_INTERVAL); + + // done; we can finally release the wakelock + Slog.i(TAG, "Restore complete."); + mWakelock.release(); + } + + // Call asynchronously into the app, passing it the restore data. The next step + // after this is always a callback, either operationComplete() or handleTimeout(). + void initiateOneRestore(PackageInfo app, int appVersionCode, IBackupAgent agent, + boolean needFullBackup) { + mCurrentPackage = app; + final String packageName = app.packageName; + + if (DEBUG) Slog.d(TAG, "initiateOneRestore packageName=" + packageName); + + // !!! TODO: get the dirs from the transport + mBackupDataName = new File(mDataDir, packageName + ".restore"); + mNewStateName = new File(mStateDir, packageName + ".new"); + mSavedStateName = new File(mStateDir, packageName); + + final int token = generateToken(); + try { + // Run the transport's restore pass + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SElinux restorecon failed for " + mBackupDataName); + } + + if (mTransport.getRestoreData(mBackupData) != BackupConstants.TRANSPORT_OK) { + // Transport-level failure, so we wind everything up and + // terminate the restore operation. + Slog.e(TAG, "Error getting restore data for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mBackupData.close(); + mBackupDataName.delete(); + executeNextState(RestoreState.FINAL); + return; + } + + // Okay, we have the data. Now have the agent do the restore. + mBackupData.close(); + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_ONLY); + + mNewState = ParcelFileDescriptor.open(mNewStateName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + // Kick off the restore, checking for hung agents + prepareOperationTimeout(token, TIMEOUT_RESTORE_INTERVAL, this); + agent.doRestore(mBackupData, appVersionCode, mNewState, token, mBackupManagerBinder); + } catch (Exception e) { + Slog.e(TAG, "Unable to call app for restore: " + packageName, e); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, e.toString()); + agentErrorCleanup(); // clears any pending timeout messages as well + + // After a restore failure we go back to running the queue. If there + // are no more packages to be restored that will be handled by the + // next step. + executeNextState(RestoreState.RUNNING_QUEUE); + } + } + + void agentErrorCleanup() { + // If the agent fails restore, it might have put the app's data + // into an incoherent state. For consistency we wipe its data + // again in this case before continuing with normal teardown + clearApplicationDataSynchronous(mCurrentPackage.packageName); + agentCleanup(); + } + + void agentCleanup() { + mBackupDataName.delete(); + try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} + try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} + mBackupData = mNewState = null; + + // if everything went okay, remember the recorded state now + // + // !!! TODO: the restored data should be migrated on the server + // side into the current dataset. In that case the new state file + // we just created would reflect the data already extant in the + // backend, so there'd be nothing more to do. Until that happens, + // however, we need to make sure that we record the data to the + // current backend dataset. (Yes, this means shipping the data over + // the wire in both directions. That's bad, but consistency comes + // first, then efficiency.) Once we introduce server-side data + // migration to the newly-restored device's dataset, we will change + // the following from a discard of the newly-written state to the + // "correct" operation of renaming into the canonical state blob. + mNewStateName.delete(); // TODO: remove; see above comment + //mNewStateName.renameTo(mSavedStateName); // TODO: replace with this + + // If this wasn't the PM pseudopackage, tear down the agent side + if (mCurrentPackage.applicationInfo != null) { + // unbind and tidy up even on timeout or failure + try { + mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); + + // The agent was probably running with a stub Application object, + // which isn't a valid run mode for the main app logic. Shut + // down the app so that next time it's launched, it gets the + // usual full initialization. Note that this is only done for + // full-system restores: when a single app has requested a restore, + // it is explicitly not killed following that operation. + if (mTargetPackage == null && (mCurrentPackage.applicationInfo.flags + & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0) { + if (DEBUG) Slog.d(TAG, "Restore complete, killing host process of " + + mCurrentPackage.applicationInfo.processName); + mActivityManager.killApplicationProcess( + mCurrentPackage.applicationInfo.processName, + mCurrentPackage.applicationInfo.uid); + } + } catch (RemoteException e) { + // can't happen; we run in the same process as the activity manager + } + } + + // The caller is responsible for reestablishing the state machine; our + // responsibility here is to clear the decks for whatever comes next. + mBackupHandler.removeMessages(MSG_TIMEOUT, this); + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + } + + // A call to agent.doRestore() has been positively acknowledged as complete + @Override + public void operationComplete() { + int size = (int) mBackupDataName.length(); + EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE, mCurrentPackage.packageName, size); + // Just go back to running the restore queue + agentCleanup(); + + executeNextState(RestoreState.RUNNING_QUEUE); + } + + // A call to agent.doRestore() has timed out + @Override + public void handleTimeout() { + Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, + mCurrentPackage.packageName, "restore timeout"); + // Handle like an agent that threw on invocation: wipe it and go on to the next + agentErrorCleanup(); + executeNextState(RestoreState.RUNNING_QUEUE); + } + + void executeNextState(RestoreState nextState) { + if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " + + this + " nextState=" + nextState); + mCurrentState = nextState; + Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); + mBackupHandler.sendMessage(msg); + } + } + + class PerformClearTask implements Runnable { + IBackupTransport mTransport; + PackageInfo mPackage; + + PerformClearTask(IBackupTransport transport, PackageInfo packageInfo) { + mTransport = transport; + mPackage = packageInfo; + } + + public void run() { + try { + // Clear the on-device backup state to ensure a full backup next time + File stateDir = new File(mBaseStateDir, mTransport.transportDirName()); + File stateFile = new File(stateDir, mPackage.packageName); + stateFile.delete(); + + // Tell the transport to remove all the persistent storage for the app + // TODO - need to handle failures + mTransport.clearBackupData(mPackage); + } catch (RemoteException e) { + // can't happen; the transport is local + } catch (Exception e) { + Slog.e(TAG, "Transport threw attempting to clear data for " + mPackage); + } finally { + try { + // TODO - need to handle failures + mTransport.finishBackup(); + } catch (RemoteException e) { + // can't happen; the transport is local + } + + // Last but not least, release the cpu + mWakelock.release(); + } + } + } + + class PerformInitializeTask implements Runnable { + HashSet mQueue; + + PerformInitializeTask(HashSet transportNames) { + mQueue = transportNames; + } + + public void run() { + try { + for (String transportName : mQueue) { + IBackupTransport transport = getTransport(transportName); + if (transport == null) { + Slog.e(TAG, "Requested init for " + transportName + " but not found"); + continue; + } + + Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName); + EventLog.writeEvent(EventLogTags.BACKUP_START, transport.transportDirName()); + long startRealtime = SystemClock.elapsedRealtime(); + int status = transport.initializeDevice(); + + if (status == BackupConstants.TRANSPORT_OK) { + status = transport.finishBackup(); + } + + // Okay, the wipe really happened. Clean up our local bookkeeping. + if (status == BackupConstants.TRANSPORT_OK) { + Slog.i(TAG, "Device init successful"); + int millis = (int) (SystemClock.elapsedRealtime() - startRealtime); + EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); + resetBackupState(new File(mBaseStateDir, transport.transportDirName())); + EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis); + synchronized (mQueueLock) { + recordInitPendingLocked(false, transportName); + } + } else { + // If this didn't work, requeue this one and try again + // after a suitable interval + Slog.e(TAG, "Transport error in initializeDevice()"); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + synchronized (mQueueLock) { + recordInitPendingLocked(true, transportName); + } + // do this via another alarm to make sure of the wakelock states + long delay = transport.requestBackupTime(); + if (DEBUG) Slog.w(TAG, "init failed on " + + transportName + " resched in " + delay); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + delay, mRunInitIntent); + } + } + } catch (RemoteException e) { + // can't happen; the transports are local + } catch (Exception e) { + Slog.e(TAG, "Unexpected error performing init", e); + } finally { + // Done; release the wakelock + mWakelock.release(); + } + } + } + + private void dataChangedImpl(String packageName) { + HashSet targets = dataChangedTargets(packageName); + dataChangedImpl(packageName, targets); + } + + private void dataChangedImpl(String packageName, HashSet targets) { + // Record that we need a backup pass for the caller. Since multiple callers + // may share a uid, we need to note all candidates within that uid and schedule + // a backup pass for each of them. + EventLog.writeEvent(EventLogTags.BACKUP_DATA_CHANGED, packageName); + + if (targets == null) { + Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" + + " uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mQueueLock) { + // Note that this client has made data changes that need to be backed up + if (targets.contains(packageName)) { + // Add the caller to the set of pending backups. If there is + // one already there, then overwrite it, but no harm done. + BackupRequest req = new BackupRequest(packageName); + if (mPendingBackups.put(packageName, req) == null) { + if (DEBUG) Slog.d(TAG, "Now staging backup of " + packageName); + + // Journal this request in case of crash. The put() + // operation returned null when this package was not already + // in the set; we want to avoid touching the disk redundantly. + writeToJournalLocked(packageName); + + if (MORE_DEBUG) { + int numKeys = mPendingBackups.size(); + Slog.d(TAG, "Now awaiting backup for " + numKeys + " participants:"); + for (BackupRequest b : mPendingBackups.values()) { + Slog.d(TAG, " + " + b); + } + } + } + } + } + } + + // Note: packageName is currently unused, but may be in the future + private HashSet dataChangedTargets(String packageName) { + // If the caller does not hold the BACKUP permission, it can only request a + // backup of its own data. + if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), + Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { + synchronized (mBackupParticipants) { + return mBackupParticipants.get(Binder.getCallingUid()); + } + } + + // a caller with full permission can ask to back up any participating app + // !!! TODO: allow backup of ANY app? + HashSet targets = new HashSet(); + synchronized (mBackupParticipants) { + int N = mBackupParticipants.size(); + for (int i = 0; i < N; i++) { + HashSet s = mBackupParticipants.valueAt(i); + if (s != null) { + targets.addAll(s); + } + } + } + return targets; + } + + private void writeToJournalLocked(String str) { + RandomAccessFile out = null; + try { + if (mJournal == null) mJournal = File.createTempFile("journal", null, mJournalDir); + out = new RandomAccessFile(mJournal, "rws"); + out.seek(out.length()); + out.writeUTF(str); + } catch (IOException e) { + Slog.e(TAG, "Can't write " + str + " to backup journal", e); + mJournal = null; + } finally { + try { if (out != null) out.close(); } catch (IOException e) {} + } + } + + // ----- IBackupManager binder interface ----- + + public void dataChanged(final String packageName) { + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + // App is running under a non-owner user profile. For now, we do not back + // up data from secondary user profiles. + // TODO: backups for all user profiles. + if (MORE_DEBUG) { + Slog.v(TAG, "dataChanged(" + packageName + ") ignored because it's user " + + callingUserHandle); + } + return; + } + + final HashSet targets = dataChangedTargets(packageName); + if (targets == null) { + Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" + + " uid=" + Binder.getCallingUid()); + return; + } + + mBackupHandler.post(new Runnable() { + public void run() { + dataChangedImpl(packageName, targets); + } + }); + } + + // Clear the given package's backup data from the current transport + public void clearBackupData(String transportName, String packageName) { + if (DEBUG) Slog.v(TAG, "clearBackupData() of " + packageName + " on " + transportName); + PackageInfo info; + try { + info = mPackageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Slog.d(TAG, "No such package '" + packageName + "' - not clearing backup data"); + return; + } + + // If the caller does not hold the BACKUP permission, it can only request a + // wipe of its own backed-up data. + HashSet apps; + if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), + Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { + apps = mBackupParticipants.get(Binder.getCallingUid()); + } else { + // a caller with full permission can ask to back up any participating app + // !!! TODO: allow data-clear of ANY app? + if (DEBUG) Slog.v(TAG, "Privileged caller, allowing clear of other apps"); + apps = new HashSet(); + int N = mBackupParticipants.size(); + for (int i = 0; i < N; i++) { + HashSet s = mBackupParticipants.valueAt(i); + if (s != null) { + apps.addAll(s); + } + } + } + + // Is the given app an available participant? + if (apps.contains(packageName)) { + // found it; fire off the clear request + if (DEBUG) Slog.v(TAG, "Found the app - running clear process"); + mBackupHandler.removeMessages(MSG_RETRY_CLEAR); + synchronized (mQueueLock) { + final IBackupTransport transport = getTransport(transportName); + if (transport == null) { + // transport is currently unavailable -- make sure to retry + Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR, + new ClearRetryParams(transportName, packageName)); + mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL); + return; + } + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_CLEAR, + new ClearParams(transport, info)); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + } + } + } + + // Run a backup pass immediately for any applications that have declared + // that they have pending updates. + public void backupNow() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow"); + + if (DEBUG) Slog.v(TAG, "Scheduling immediate backup pass"); + synchronized (mQueueLock) { + // Because the alarms we are using can jitter, and we want an *immediate* + // backup pass to happen, we restart the timer beginning with "next time," + // then manually fire the backup trigger intent ourselves. + startBackupAlarmsLocked(BACKUP_INTERVAL); + try { + mRunBackupIntent.send(); + } catch (PendingIntent.CanceledException e) { + // should never happen + Slog.e(TAG, "run-backup intent cancelled!"); + } + } + } + + boolean deviceIsProvisioned() { + final ContentResolver resolver = mContext.getContentResolver(); + return (Settings.Global.getInt(resolver, Settings.Global.DEVICE_PROVISIONED, 0) != 0); + } + + // Run a *full* backup pass for the given package, writing the resulting data stream + // to the supplied file descriptor. This method is synchronous and does not return + // to the caller until the backup has been completed. + public void fullBackup(ParcelFileDescriptor fd, boolean includeApks, + boolean includeObbs, boolean includeShared, + boolean doAllApps, boolean includeSystem, String[] pkgList) { + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup"); + + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + throw new IllegalStateException("Backup supported only for the device owner"); + } + + // Validate + if (!doAllApps) { + if (!includeShared) { + // If we're backing up shared data (sdcard or equivalent), then we can run + // without any supplied app names. Otherwise, we'd be doing no work, so + // report the error. + if (pkgList == null || pkgList.length == 0) { + throw new IllegalArgumentException( + "Backup requested but neither shared nor any apps named"); + } + } + } + + long oldId = Binder.clearCallingIdentity(); + try { + // Doesn't make sense to do a full backup prior to setup + if (!deviceIsProvisioned()) { + Slog.i(TAG, "Full backup not supported before setup"); + return; + } + + if (DEBUG) Slog.v(TAG, "Requesting full backup: apks=" + includeApks + + " obb=" + includeObbs + " shared=" + includeShared + " all=" + doAllApps + + " pkgs=" + pkgList); + Slog.i(TAG, "Beginning full backup..."); + + FullBackupParams params = new FullBackupParams(fd, includeApks, includeObbs, + includeShared, doAllApps, includeSystem, pkgList); + final int token = generateToken(); + synchronized (mFullConfirmations) { + mFullConfirmations.put(token, params); + } + + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full backup confirmation"); + mFullConfirmations.delete(token); + return; + } + + // make sure the screen is lit for the user interaction + mPowerManager.userActivity(SystemClock.uptimeMillis(), false); + + // start the confirmation countdown + startConfirmationTimeout(token, params); + + // wait for the backup to be performed + if (DEBUG) Slog.d(TAG, "Waiting for full backup completion..."); + waitForCompletion(params); + } finally { + try { + fd.close(); + } catch (IOException e) { + // just eat it + } + Binder.restoreCallingIdentity(oldId); + Slog.d(TAG, "Full backup processing complete."); + } + } + + public void fullRestore(ParcelFileDescriptor fd) { + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullRestore"); + + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + throw new IllegalStateException("Restore supported only for the device owner"); + } + + long oldId = Binder.clearCallingIdentity(); + + try { + // Check whether the device has been provisioned -- we don't handle + // full restores prior to completing the setup process. + if (!deviceIsProvisioned()) { + Slog.i(TAG, "Full restore not permitted before setup"); + return; + } + + Slog.i(TAG, "Beginning full restore..."); + + FullRestoreParams params = new FullRestoreParams(fd); + final int token = generateToken(); + synchronized (mFullConfirmations) { + mFullConfirmations.put(token, params); + } + + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full restore confirmation"); + mFullConfirmations.delete(token); + return; + } + + // make sure the screen is lit for the user interaction + mPowerManager.userActivity(SystemClock.uptimeMillis(), false); + + // start the confirmation countdown + startConfirmationTimeout(token, params); + + // wait for the restore to be performed + if (DEBUG) Slog.d(TAG, "Waiting for full restore completion..."); + waitForCompletion(params); + } finally { + try { + fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Error trying to close fd after full restore: " + e); + } + Binder.restoreCallingIdentity(oldId); + Slog.i(TAG, "Full restore processing complete."); + } + } + + boolean startConfirmationUi(int token, String action) { + try { + Intent confIntent = new Intent(action); + confIntent.setClassName("com.android.backupconfirm", + "com.android.backupconfirm.BackupRestoreConfirmation"); + confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); + confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(confIntent); + } catch (ActivityNotFoundException e) { + return false; + } + return true; + } + + void startConfirmationTimeout(int token, FullParams params) { + if (MORE_DEBUG) Slog.d(TAG, "Posting conf timeout msg after " + + TIMEOUT_FULL_CONFIRMATION + " millis"); + Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, + token, 0, params); + mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); + } + + void waitForCompletion(FullParams params) { + synchronized (params.latch) { + while (params.latch.get() == false) { + try { + params.latch.wait(); + } catch (InterruptedException e) { /* never interrupted */ } + } + } + } + + void signalFullBackupRestoreCompletion(FullParams params) { + synchronized (params.latch) { + params.latch.set(true); + params.latch.notifyAll(); + } + } + + // Confirm that the previously-requested full backup/restore operation can proceed. This + // is used to require a user-facing disclosure about the operation. + @Override + public void acknowledgeFullBackupOrRestore(int token, boolean allow, + String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { + if (DEBUG) Slog.d(TAG, "acknowledgeFullBackupOrRestore : token=" + token + + " allow=" + allow); + + // TODO: possibly require not just this signature-only permission, but even + // require that the specific designated confirmation-UI app uid is the caller? + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "acknowledgeFullBackupOrRestore"); + + long oldId = Binder.clearCallingIdentity(); + try { + + FullParams params; + synchronized (mFullConfirmations) { + params = mFullConfirmations.get(token); + if (params != null) { + mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params); + mFullConfirmations.delete(token); + + if (allow) { + final int verb = params instanceof FullBackupParams + ? MSG_RUN_FULL_BACKUP + : MSG_RUN_FULL_RESTORE; + + params.observer = observer; + params.curPassword = curPassword; + + boolean isEncrypted; + try { + isEncrypted = (mMountService.getEncryptionState() != + IMountService.ENCRYPTION_STATE_NONE); + if (isEncrypted) Slog.w(TAG, "Device is encrypted; forcing enc password"); + } catch (RemoteException e) { + // couldn't contact the mount service; fail "safe" and assume encryption + Slog.e(TAG, "Unable to contact mount service!"); + isEncrypted = true; + } + params.encryptPassword = (isEncrypted) ? curPassword : encPpassword; + + if (DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(verb, params); + mBackupHandler.sendMessage(msg); + } else { + Slog.w(TAG, "User rejected full backup/restore operation"); + // indicate completion without having actually transferred any data + signalFullBackupRestoreCompletion(params); + } + } else { + Slog.w(TAG, "Attempted to ack full backup/restore with invalid token"); + } + } + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + // Enable/disable the backup service + @Override + public void setBackupEnabled(boolean enable) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupEnabled"); + + Slog.i(TAG, "Backup enabled => " + enable); + + long oldId = Binder.clearCallingIdentity(); + try { + boolean wasEnabled = mEnabled; + synchronized (this) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.BACKUP_ENABLED, enable ? 1 : 0); + mEnabled = enable; + } + + synchronized (mQueueLock) { + if (enable && !wasEnabled && mProvisioned) { + // if we've just been enabled, start scheduling backup passes + startBackupAlarmsLocked(BACKUP_INTERVAL); + } else if (!enable) { + // No longer enabled, so stop running backups + if (DEBUG) Slog.i(TAG, "Opting out of backup"); + + mAlarmManager.cancel(mRunBackupIntent); + + // This also constitutes an opt-out, so we wipe any data for + // this device from the backend. We start that process with + // an alarm in order to guarantee wakelock states. + if (wasEnabled && mProvisioned) { + // NOTE: we currently flush every registered transport, not just + // the currently-active one. + HashSet allTransports; + synchronized (mTransports) { + allTransports = new HashSet(mTransports.keySet()); + } + // build the set of transports for which we are posting an init + for (String transport : allTransports) { + recordInitPendingLocked(true, transport); + } + mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), + mRunInitIntent); + } + } + } + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + // Enable/disable automatic restore of app data at install time + public void setAutoRestore(boolean doAutoRestore) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setAutoRestore"); + + Slog.i(TAG, "Auto restore => " + doAutoRestore); + + synchronized (this) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0); + mAutoRestore = doAutoRestore; + } + } + + // Mark the backup service as having been provisioned + public void setBackupProvisioned(boolean available) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupProvisioned"); + /* + * This is now a no-op; provisioning is simply the device's own setup state. + */ + } + + private void startBackupAlarmsLocked(long delayBeforeFirstBackup) { + // We used to use setInexactRepeating(), but that may be linked to + // backups running at :00 more often than not, creating load spikes. + // Schedule at an exact time for now, and also add a bit of "fuzz". + + Random random = new Random(); + long when = System.currentTimeMillis() + delayBeforeFirstBackup + + random.nextInt(FUZZ_MILLIS); + mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, when, + BACKUP_INTERVAL + random.nextInt(FUZZ_MILLIS), mRunBackupIntent); + mNextBackupPass = when; + } + + // Report whether the backup mechanism is currently enabled + public boolean isBackupEnabled() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "isBackupEnabled"); + return mEnabled; // no need to synchronize just to read it + } + + // Report the name of the currently active transport + public String getCurrentTransport() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getCurrentTransport"); + if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + mCurrentTransport); + return mCurrentTransport; + } + + // Report all known, available backup transports + public String[] listAllTransports() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "listAllTransports"); + + String[] list = null; + ArrayList known = new ArrayList(); + for (Map.Entry entry : mTransports.entrySet()) { + if (entry.getValue() != null) { + known.add(entry.getKey()); + } + } + + if (known.size() > 0) { + list = new String[known.size()]; + known.toArray(list); + } + return list; + } + + // Select which transport to use for the next backup operation. If the given + // name is not one of the available transports, no action is taken and the method + // returns null. + public String selectBackupTransport(String transport) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "selectBackupTransport"); + + synchronized (mTransports) { + String prevTransport = null; + if (mTransports.get(transport) != null) { + prevTransport = mCurrentTransport; + mCurrentTransport = transport; + Settings.Secure.putString(mContext.getContentResolver(), + Settings.Secure.BACKUP_TRANSPORT, transport); + Slog.v(TAG, "selectBackupTransport() set " + mCurrentTransport + + " returning " + prevTransport); + } else { + Slog.w(TAG, "Attempt to select unavailable transport " + transport); + } + return prevTransport; + } + } + + // Supply the configuration Intent for the given transport. If the name is not one + // of the available transports, or if the transport does not supply any configuration + // UI, the method returns null. + public Intent getConfigurationIntent(String transportName) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getConfigurationIntent"); + + synchronized (mTransports) { + final IBackupTransport transport = mTransports.get(transportName); + if (transport != null) { + try { + final Intent intent = transport.configurationIntent(); + if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent " + + intent); + return intent; + } catch (RemoteException e) { + /* fall through to return null */ + } + } + } + + return null; + } + + // Supply the configuration summary string for the given transport. If the name is + // not one of the available transports, or if the transport does not supply any + // summary / destination string, the method can return null. + // + // This string is used VERBATIM as the summary text of the relevant Settings item! + public String getDestinationString(String transportName) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getDestinationString"); + + synchronized (mTransports) { + final IBackupTransport transport = mTransports.get(transportName); + if (transport != null) { + try { + final String text = transport.currentDestinationString(); + if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text); + return text; + } catch (RemoteException e) { + /* fall through to return null */ + } + } + } + + return null; + } + + // Callback: a requested backup agent has been instantiated. This should only + // be called from the Activity Manager. + public void agentConnected(String packageName, IBinder agentBinder) { + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + Slog.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); + IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder); + mConnectedAgent = agent; + mConnecting = false; + } else { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent connected"); + } + mAgentConnectLock.notifyAll(); + } + } + + // Callback: a backup agent has failed to come up, or has unexpectedly quit. + // If the agent failed to come up in the first place, the agentBinder argument + // will be null. This should only be called from the Activity Manager. + public void agentDisconnected(String packageName) { + // TODO: handle backup being interrupted + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + mConnectedAgent = null; + mConnecting = false; + } else { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent disconnected"); + } + mAgentConnectLock.notifyAll(); + } + } + + // An application being installed will need a restore pass, then the Package Manager + // will need to be told when the restore is finished. + public void restoreAtInstall(String packageName, int token) { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " attemping install-time restore"); + return; + } + + long restoreSet = getAvailableRestoreToken(packageName); + if (DEBUG) Slog.v(TAG, "restoreAtInstall pkg=" + packageName + + " token=" + Integer.toHexString(token) + + " restoreSet=" + Long.toHexString(restoreSet)); + + if (mAutoRestore && mProvisioned && restoreSet != 0) { + // Do we have a transport to fetch data for us? + IBackupTransport transport = getTransport(mCurrentTransport); + if (transport == null) { + if (DEBUG) Slog.w(TAG, "No transport for install-time restore"); + return; + } + + try { + // okay, we're going to attempt a restore of this package from this restore set. + // The eventual message back into the Package Manager to run the post-install + // steps for 'token' will be issued from the restore handling code. + + // This can throw and so *must* happen before the wakelock is acquired + String dirName = transport.transportDirName(); + + // We can use a synthetic PackageInfo here because: + // 1. We know it's valid, since the Package Manager supplied the name + // 2. Only the packageName field will be used by the restore code + PackageInfo pkg = new PackageInfo(); + pkg.packageName = packageName; + + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(transport, dirName, null, + restoreSet, pkg, token, true); + mBackupHandler.sendMessage(msg); + } catch (RemoteException e) { + // Binding to the transport broke; back off and proceed with the installation. + Slog.e(TAG, "Unable to contact transport for install-time restore"); + } + } else { + // Auto-restore disabled or no way to attempt a restore; just tell the Package + // Manager to proceed with the post-install handling for this package. + if (DEBUG) Slog.v(TAG, "No restore set -- skipping restore"); + try { + mPackageManagerBinder.finishPackageInstall(token); + } catch (RemoteException e) { /* can't happen */ } + } + } + + // Hand off a restore session + public IRestoreSession beginRestoreSession(String packageName, String transport) { + if (DEBUG) Slog.v(TAG, "beginRestoreSession: pkg=" + packageName + + " transport=" + transport); + + boolean needPermission = true; + if (transport == null) { + transport = mCurrentTransport; + + if (packageName != null) { + PackageInfo app = null; + try { + app = mPackageManager.getPackageInfo(packageName, 0); + } catch (NameNotFoundException nnf) { + Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); + throw new IllegalArgumentException("Package " + packageName + " not found"); + } + + if (app.applicationInfo.uid == Binder.getCallingUid()) { + // So: using the current active transport, and the caller has asked + // that its own package will be restored. In this narrow use case + // we do not require the caller to hold the permission. + needPermission = false; + } + } + } + + if (needPermission) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "beginRestoreSession"); + } else { + if (DEBUG) Slog.d(TAG, "restoring self on current transport; no permission needed"); + } + + synchronized(this) { + if (mActiveRestoreSession != null) { + Slog.d(TAG, "Restore session requested but one already active"); + return null; + } + mActiveRestoreSession = new ActiveRestoreSession(packageName, transport); + mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); + } + return mActiveRestoreSession; + } + + void clearRestoreSession(ActiveRestoreSession currentSession) { + synchronized(this) { + if (currentSession != mActiveRestoreSession) { + Slog.e(TAG, "ending non-current restore session"); + } else { + if (DEBUG) Slog.v(TAG, "Clearing restore session and halting timeout"); + mActiveRestoreSession = null; + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + } + } + } + + // Note that a currently-active backup agent has notified us that it has + // completed the given outstanding asynchronous backup/restore operation. + @Override + public void opComplete(int token) { + if (MORE_DEBUG) Slog.v(TAG, "opComplete: " + Integer.toHexString(token)); + Operation op = null; + synchronized (mCurrentOpLock) { + op = mCurrentOperations.get(token); + if (op != null) { + op.state = OP_ACKNOWLEDGED; + } + mCurrentOpLock.notifyAll(); + } + + // The completion callback, if any, is invoked on the handler + if (op != null && op.callback != null) { + Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, op.callback); + mBackupHandler.sendMessage(msg); + } + } + + // ----- Restore session ----- + + class ActiveRestoreSession extends IRestoreSession.Stub { + private static final String TAG = "RestoreSession"; + + private String mPackageName; + private IBackupTransport mRestoreTransport = null; + RestoreSet[] mRestoreSets = null; + boolean mEnded = false; + + ActiveRestoreSession(String packageName, String transport) { + mPackageName = packageName; + mRestoreTransport = getTransport(transport); + } + + // --- Binder interface --- + public synchronized int getAvailableRestoreSets(IRestoreObserver observer) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getAvailableRestoreSets"); + if (observer == null) { + throw new IllegalArgumentException("Observer must not be null"); + } + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + long oldId = Binder.clearCallingIdentity(); + try { + if (mRestoreTransport == null) { + Slog.w(TAG, "Null transport getting restore sets"); + return -1; + } + // spin off the transport request to our service thread + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_GET_RESTORE_SETS, + new RestoreGetSetsParams(mRestoreTransport, this, observer)); + mBackupHandler.sendMessage(msg); + return 0; + } catch (Exception e) { + Slog.e(TAG, "Error in getAvailableRestoreSets", e); + return -1; + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + public synchronized int restoreAll(long token, IRestoreObserver observer) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "performRestore"); + + if (DEBUG) Slog.d(TAG, "restoreAll token=" + Long.toHexString(token) + + " observer=" + observer); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mRestoreTransport == null || mRestoreSets == null) { + Slog.e(TAG, "Ignoring restoreAll() with no restore set"); + return -1; + } + + if (mPackageName != null) { + Slog.e(TAG, "Ignoring restoreAll() on single-package session"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + synchronized (mQueueLock) { + for (int i = 0; i < mRestoreSets.length; i++) { + if (token == mRestoreSets[i].token) { + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, + observer, token, true); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + } + } + + Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); + return -1; + } + + public synchronized int restoreSome(long token, IRestoreObserver observer, + String[] packages) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "performRestore"); + + if (DEBUG) { + StringBuilder b = new StringBuilder(128); + b.append("restoreSome token="); + b.append(Long.toHexString(token)); + b.append(" observer="); + b.append(observer.toString()); + b.append(" packages="); + if (packages == null) { + b.append("null"); + } else { + b.append('{'); + boolean first = true; + for (String s : packages) { + if (!first) { + b.append(", "); + } else first = false; + b.append(s); + } + b.append('}'); + } + Slog.d(TAG, b.toString()); + } + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mRestoreTransport == null || mRestoreSets == null) { + Slog.e(TAG, "Ignoring restoreAll() with no restore set"); + return -1; + } + + if (mPackageName != null) { + Slog.e(TAG, "Ignoring restoreAll() on single-package session"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + synchronized (mQueueLock) { + for (int i = 0; i < mRestoreSets.length; i++) { + if (token == mRestoreSets[i].token) { + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, token, + packages, true); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + } + } + + Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); + return -1; + } + + public synchronized int restorePackage(String packageName, IRestoreObserver observer) { + if (DEBUG) Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mPackageName != null) { + if (! mPackageName.equals(packageName)) { + Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName + + " on session for package " + mPackageName); + return -1; + } + } + + PackageInfo app = null; + try { + app = mPackageManager.getPackageInfo(packageName, 0); + } catch (NameNotFoundException nnf) { + Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); + return -1; + } + + // If the caller is not privileged and is not coming from the target + // app's uid, throw a permission exception back to the caller. + int perm = mContext.checkPermission(android.Manifest.permission.BACKUP, + Binder.getCallingPid(), Binder.getCallingUid()); + if ((perm == PackageManager.PERMISSION_DENIED) && + (app.applicationInfo.uid != Binder.getCallingUid())) { + Slog.w(TAG, "restorePackage: bad packageName=" + packageName + + " or calling uid=" + Binder.getCallingUid()); + throw new SecurityException("No permission to restore other packages"); + } + + // If the package has no backup agent, we obviously cannot proceed + if (app.applicationInfo.backupAgentName == null) { + Slog.w(TAG, "Asked to restore package " + packageName + " with no agent"); + return -1; + } + + // So far so good; we're allowed to try to restore this package. Now + // check whether there is data for it in the current dataset, falling back + // to the ancestral dataset if not. + long token = getAvailableRestoreToken(packageName); + + // If we didn't come up with a place to look -- no ancestral dataset and + // the app has never been backed up from this device -- there's nothing + // to do but return failure. + if (token == 0) { + if (DEBUG) Slog.w(TAG, "No data available for this package; not restoring"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + // Ready to go: enqueue the restore request and claim success + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, + observer, token, app, 0, false); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + + // Posted to the handler to tear down a restore session in a cleanly synchronized way + class EndRestoreRunnable implements Runnable { + BackupManagerService mBackupManager; + ActiveRestoreSession mSession; + + EndRestoreRunnable(BackupManagerService manager, ActiveRestoreSession session) { + mBackupManager = manager; + mSession = session; + } + + public void run() { + // clean up the session's bookkeeping + synchronized (mSession) { + try { + if (mSession.mRestoreTransport != null) { + mSession.mRestoreTransport.finishRestore(); + } + } catch (Exception e) { + Slog.e(TAG, "Error in finishRestore", e); + } finally { + mSession.mRestoreTransport = null; + mSession.mEnded = true; + } + } + + // clean up the BackupManagerService side of the bookkeeping + // and cancel any pending timeout message + mBackupManager.clearRestoreSession(mSession); + } + } + + public synchronized void endRestoreSession() { + if (DEBUG) Slog.d(TAG, "endRestoreSession"); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + mBackupHandler.post(new EndRestoreRunnable(BackupManagerService.this, this)); + } + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + long identityToken = Binder.clearCallingIdentity(); + try { + dumpInternal(pw); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } + + private void dumpInternal(PrintWriter pw) { + synchronized (mQueueLock) { + pw.println("Backup Manager is " + (mEnabled ? "enabled" : "disabled") + + " / " + (!mProvisioned ? "not " : "") + "provisioned / " + + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init"); + pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled")); + if (mBackupRunning) pw.println("Backup currently running"); + pw.println("Last backup pass started: " + mLastBackupPass + + " (now = " + System.currentTimeMillis() + ')'); + pw.println(" next scheduled: " + mNextBackupPass); + + pw.println("Available transports:"); + for (String t : listAllTransports()) { + pw.println((t.equals(mCurrentTransport) ? " * " : " ") + t); + try { + IBackupTransport transport = getTransport(t); + File dir = new File(mBaseStateDir, transport.transportDirName()); + pw.println(" destination: " + transport.currentDestinationString()); + pw.println(" intent: " + transport.configurationIntent()); + for (File f : dir.listFiles()) { + pw.println(" " + f.getName() + " - " + f.length() + " state bytes"); + } + } catch (Exception e) { + Slog.e(TAG, "Error in transport", e); + pw.println(" Error: " + e); + } + } + + pw.println("Pending init: " + mPendingInits.size()); + for (String s : mPendingInits) { + pw.println(" " + s); + } + + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + if (!mBackupTrace.isEmpty()) { + pw.println("Most recent backup trace:"); + for (String s : mBackupTrace) { + pw.println(" " + s); + } + } + } + } + + int N = mBackupParticipants.size(); + pw.println("Participants:"); + for (int i=0; i participants = mBackupParticipants.valueAt(i); + for (String app: participants) { + pw.println(" " + app); + } + } + + pw.println("Ancestral packages: " + + (mAncestralPackages == null ? "none" : mAncestralPackages.size())); + if (mAncestralPackages != null) { + for (String pkg : mAncestralPackages) { + pw.println(" " + pkg); + } + } + + pw.println("Ever backed up: " + mEverStoredApps.size()); + for (String pkg : mEverStoredApps) { + pw.println(" " + pkg); + } + + pw.println("Pending backup: " + mPendingBackups.size()); + for (BackupRequest req : mPendingBackups.values()) { + pw.println(" " + req); + } + } + } +} diff --git a/services/java/com/android/server/backup/PackageManagerBackupAgent.java b/services/java/com/android/server/backup/PackageManagerBackupAgent.java new file mode 100644 index 000000000000..495da886fa65 --- /dev/null +++ b/services/java/com/android/server/backup/PackageManagerBackupAgent.java @@ -0,0 +1,425 @@ +/* + * 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 com.android.server.backup; + +import android.app.backup.BackupAgent; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Slog; + +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.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * We back up the signatures of each package so that during a system restore, + * we can verify that the app whose data we think we have matches the app + * actually resident on the device. + * + * Since the Package Manager isn't a proper "application" we just provide a + * direct IBackupAgent implementation and hand-construct it at need. + */ +public class PackageManagerBackupAgent extends BackupAgent { + private static final String TAG = "PMBA"; + private static final boolean DEBUG = false; + + // key under which we store global metadata (individual app metadata + // is stored using the package name as a key) + private static final String GLOBAL_METADATA_KEY = "@meta@"; + + private List mAllPackages; + private PackageManager mPackageManager; + // version & signature info of each app in a restore set + private HashMap mRestoredSignatures; + // The version info of each backed-up app as read from the state file + private HashMap mStateVersions = new HashMap(); + + private final HashSet mExisting = new HashSet(); + private int mStoredSdkVersion; + private String mStoredIncrementalVersion; + private boolean mHasMetadata; + + public class Metadata { + public int versionCode; + public Signature[] signatures; + + Metadata(int version, Signature[] sigs) { + versionCode = version; + signatures = sigs; + } + } + + // We're constructed with the set of applications that are participating + // in backup. This set changes as apps are installed & removed. + PackageManagerBackupAgent(PackageManager packageMgr, List packages) { + mPackageManager = packageMgr; + mAllPackages = packages; + mRestoredSignatures = null; + mHasMetadata = false; + } + + public boolean hasMetadata() { + return mHasMetadata; + } + + public Metadata getRestoredMetadata(String packageName) { + if (mRestoredSignatures == null) { + Slog.w(TAG, "getRestoredMetadata() before metadata read!"); + return null; + } + + return mRestoredSignatures.get(packageName); + } + + public Set getRestoredPackages() { + if (mRestoredSignatures == null) { + Slog.w(TAG, "getRestoredPackages() before metadata read!"); + return null; + } + + // This is technically the set of packages on the originating handset + // that had backup agents at all, not limited to the set of packages + // that had actually contributed a restore dataset, but it's a + // close enough approximation for our purposes and does not require any + // additional involvement by the transport to obtain. + return mRestoredSignatures.keySet(); + } + + // The backed up data is the signature block for each app, keyed by + // the package name. + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + if (DEBUG) Slog.v(TAG, "onBackup()"); + + ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); // we'll reuse these + DataOutputStream outputBufferStream = new DataOutputStream(outputBuffer); + parseStateFile(oldState); + + // If the stored version string differs, we need to re-backup all + // of the metadata. We force this by removing everything from the + // "already backed up" map built by parseStateFile(). + if (mStoredIncrementalVersion == null + || !mStoredIncrementalVersion.equals(Build.VERSION.INCREMENTAL)) { + Slog.i(TAG, "Previous metadata " + mStoredIncrementalVersion + " mismatch vs " + + Build.VERSION.INCREMENTAL + " - rewriting"); + mExisting.clear(); + } + + try { + /* + * Global metadata: + * + * int SDKversion -- the SDK version of the OS itself on the device + * that produced this backup set. Used to reject + * backups from later OSes onto earlier ones. + * String incremental -- the incremental release name of the OS stored in + * the backup set. + */ + if (!mExisting.contains(GLOBAL_METADATA_KEY)) { + if (DEBUG) Slog.v(TAG, "Storing global metadata key"); + outputBufferStream.writeInt(Build.VERSION.SDK_INT); + outputBufferStream.writeUTF(Build.VERSION.INCREMENTAL); + writeEntity(data, GLOBAL_METADATA_KEY, outputBuffer.toByteArray()); + } else { + if (DEBUG) Slog.v(TAG, "Global metadata key already stored"); + // don't consider it to have been skipped/deleted + mExisting.remove(GLOBAL_METADATA_KEY); + } + + // For each app we have on device, see if we've backed it up yet. If not, + // write its signature block to the output, keyed on the package name. + for (PackageInfo pkg : mAllPackages) { + String packName = pkg.packageName; + if (packName.equals(GLOBAL_METADATA_KEY)) { + // We've already handled the metadata key; skip it here + continue; + } else { + PackageInfo info = null; + try { + info = mPackageManager.getPackageInfo(packName, + PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + // Weird; we just found it, and now are told it doesn't exist. + // Treat it as having been removed from the device. + mExisting.add(packName); + continue; + } + + if (mExisting.contains(packName)) { + // We have backed up this app before. Check whether the version + // of the backup matches the version of the current app; if they + // don't match, the app has been updated and we need to store its + // metadata again. In either case, take it out of mExisting so that + // we don't consider it deleted later. + mExisting.remove(packName); + if (info.versionCode == mStateVersions.get(packName).versionCode) { + continue; + } + } + + if (info.signatures == null || info.signatures.length == 0) + { + Slog.w(TAG, "Not backing up package " + packName + + " since it appears to have no signatures."); + continue; + } + + // We need to store this app's metadata + /* + * Metadata for each package: + * + * int version -- [4] the package's versionCode + * byte[] signatures -- [len] flattened Signature[] of the package + */ + + // marshal the version code in a canonical form + outputBuffer.reset(); + outputBufferStream.writeInt(info.versionCode); + writeSignatureArray(outputBufferStream, info.signatures); + + if (DEBUG) { + Slog.v(TAG, "+ writing metadata for " + packName + + " version=" + info.versionCode + + " entityLen=" + outputBuffer.size()); + } + + // Now we can write the backup entity for this package + writeEntity(data, packName, outputBuffer.toByteArray()); + } + } + + // At this point, the only entries in 'existing' are apps that were + // mentioned in the saved state file, but appear to no longer be present + // on the device. Write a deletion entity for them. + for (String app : mExisting) { + if (DEBUG) Slog.v(TAG, "- removing metadata for deleted pkg " + app); + try { + data.writeEntityHeader(app, -1); + } catch (IOException e) { + Slog.e(TAG, "Unable to write package deletions!"); + return; + } + } + } catch (IOException e) { + // Real error writing data + Slog.e(TAG, "Unable to write package backup data file!"); + return; + } + + // Finally, write the new state blob -- just the list of all apps we handled + writeStateFile(mAllPackages, newState); + } + + private static void writeEntity(BackupDataOutput data, String key, byte[] bytes) + throws IOException { + data.writeEntityHeader(key, bytes.length); + data.writeEntityData(bytes, bytes.length); + } + + // "Restore" here is a misnomer. What we're really doing is reading back the + // set of app signatures associated with each backed-up app in this restore + // image. We'll use those later to determine what we can legitimately restore. + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + List restoredApps = new ArrayList(); + HashMap sigMap = new HashMap(); + if (DEBUG) Slog.v(TAG, "onRestore()"); + int storedSystemVersion = -1; + + while (data.readNextHeader()) { + String key = data.getKey(); + int dataSize = data.getDataSize(); + + if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize); + + // generic setup to parse any entity data + byte[] inputBytes = new byte[dataSize]; + data.readEntityData(inputBytes, 0, dataSize); + ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes); + DataInputStream inputBufferStream = new DataInputStream(inputBuffer); + + if (key.equals(GLOBAL_METADATA_KEY)) { + int storedSdkVersion = inputBufferStream.readInt(); + if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion); + if (storedSystemVersion > Build.VERSION.SDK_INT) { + // returning before setting the sig map means we rejected the restore set + Slog.w(TAG, "Restore set was from a later version of Android; not restoring"); + return; + } + mStoredSdkVersion = storedSdkVersion; + mStoredIncrementalVersion = inputBufferStream.readUTF(); + mHasMetadata = true; + if (DEBUG) { + Slog.i(TAG, "Restore set version " + storedSystemVersion + + " is compatible with OS version " + Build.VERSION.SDK_INT + + " (" + mStoredIncrementalVersion + " vs " + + Build.VERSION.INCREMENTAL + ")"); + } + } else { + // it's a file metadata record + int versionCode = inputBufferStream.readInt(); + Signature[] sigs = readSignatureArray(inputBufferStream); + if (DEBUG) { + Slog.i(TAG, " read metadata for " + key + + " dataSize=" + dataSize + + " versionCode=" + versionCode + " sigs=" + sigs); + } + + if (sigs == null || sigs.length == 0) { + Slog.w(TAG, "Not restoring package " + key + + " since it appears to have no signatures."); + continue; + } + + ApplicationInfo app = new ApplicationInfo(); + app.packageName = key; + restoredApps.add(app); + sigMap.put(key, new Metadata(versionCode, sigs)); + } + } + + // On successful completion, cache the signature map for the Backup Manager to use + mRestoredSignatures = sigMap; + } + + private static void writeSignatureArray(DataOutputStream out, Signature[] sigs) + throws IOException { + // write the number of signatures in the array + out.writeInt(sigs.length); + + // write the signatures themselves, length + flattened buffer + for (Signature sig : sigs) { + byte[] flat = sig.toByteArray(); + out.writeInt(flat.length); + out.write(flat); + } + } + + private static Signature[] readSignatureArray(DataInputStream in) { + try { + int num; + try { + num = in.readInt(); + } catch (EOFException e) { + // clean termination + Slog.w(TAG, "Read empty signature block"); + return null; + } + + if (DEBUG) Slog.v(TAG, " ... unflatten read " + num); + + // Sensical? + if (num > 20) { + Slog.e(TAG, "Suspiciously large sig count in restore data; aborting"); + throw new IllegalStateException("Bad restore state"); + } + + Signature[] sigs = new Signature[num]; + for (int i = 0; i < num; i++) { + int len = in.readInt(); + byte[] flatSig = new byte[len]; + in.read(flatSig); + sigs[i] = new Signature(flatSig); + } + return sigs; + } catch (IOException e) { + Slog.e(TAG, "Unable to read signatures"); + return null; + } + } + + // Util: parse out an existing state file into a usable structure + private void parseStateFile(ParcelFileDescriptor stateFile) { + mExisting.clear(); + mStateVersions.clear(); + mStoredSdkVersion = 0; + mStoredIncrementalVersion = null; + + // The state file is just the list of app names we have stored signatures for + // with the exception of the metadata block, to which is also appended the + // version numbers corresponding with the last time we wrote this PM block. + // If they mismatch the current system, we'll re-store the metadata key. + FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor()); + DataInputStream in = new DataInputStream(instream); + + int bufSize = 256; + byte[] buf = new byte[bufSize]; + try { + String pkg = in.readUTF(); + if (pkg.equals(GLOBAL_METADATA_KEY)) { + mStoredSdkVersion = in.readInt(); + mStoredIncrementalVersion = in.readUTF(); + mExisting.add(GLOBAL_METADATA_KEY); + } else { + Slog.e(TAG, "No global metadata in state file!"); + return; + } + + // The global metadata was first; now read all the apps + while (true) { + pkg = in.readUTF(); + int versionCode = in.readInt(); + mExisting.add(pkg); + mStateVersions.put(pkg, new Metadata(versionCode, null)); + } + } catch (EOFException eof) { + // safe; we're done + } catch (IOException e) { + // whoops, bad state file. abort. + Slog.e(TAG, "Unable to read Package Manager state file: " + e); + } + } + + // Util: write out our new backup state file + private void writeStateFile(List pkgs, ParcelFileDescriptor stateFile) { + FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor()); + DataOutputStream out = new DataOutputStream(outstream); + + try { + // by the time we get here we know we've stored the global metadata record + out.writeUTF(GLOBAL_METADATA_KEY); + out.writeInt(Build.VERSION.SDK_INT); + out.writeUTF(Build.VERSION.INCREMENTAL); + + // now write all the app names too + for (PackageInfo pkg : pkgs) { + out.writeUTF(pkg.packageName); + out.writeInt(pkg.versionCode); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to write package manager state file!"); + return; + } + } +} diff --git a/services/java/com/android/server/backup/SystemBackupAgent.java b/services/java/com/android/server/backup/SystemBackupAgent.java new file mode 100644 index 000000000000..26e2e2ac356c --- /dev/null +++ b/services/java/com/android/server/backup/SystemBackupAgent.java @@ -0,0 +1,185 @@ +/* + * 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 com.android.server.backup; + + +import android.app.IWallpaperManager; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupAgentHelper; +import android.app.backup.FullBackup; +import android.app.backup.FullBackupDataOutput; +import android.app.backup.WallpaperBackupHelper; +import android.content.Context; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; + +/** + * Backup agent for various system-managed data, currently just the system wallpaper + */ +public class SystemBackupAgent extends BackupAgentHelper { + private static final String TAG = "SystemBackupAgent"; + + // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME + // are also used in the full-backup file format, so must not change unless steps are + // taken to support the legacy backed-up datasets. + private static final String WALLPAPER_IMAGE_FILENAME = "wallpaper"; + private static final String WALLPAPER_INFO_FILENAME = "wallpaper_info.xml"; + + // TODO: Will need to change if backing up non-primary user's wallpaper + private static final String WALLPAPER_IMAGE_DIR = + Environment.getUserSystemDirectory(UserHandle.USER_OWNER).getAbsolutePath(); + private static final String WALLPAPER_IMAGE = WallpaperBackupHelper.WALLPAPER_IMAGE; + + // TODO: Will need to change if backing up non-primary user's wallpaper + private static final String WALLPAPER_INFO_DIR = + Environment.getUserSystemDirectory(UserHandle.USER_OWNER).getAbsolutePath(); + private static final String WALLPAPER_INFO = WallpaperBackupHelper.WALLPAPER_INFO; + // Use old keys to keep legacy data compatibility and avoid writing two wallpapers + private static final String WALLPAPER_IMAGE_KEY = WallpaperBackupHelper.WALLPAPER_IMAGE_KEY; + private static final String WALLPAPER_INFO_KEY = WallpaperBackupHelper.WALLPAPER_INFO_KEY; + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + // We only back up the data under the current "wallpaper" schema with metadata + IWallpaperManager wallpaper = (IWallpaperManager)ServiceManager.getService( + Context.WALLPAPER_SERVICE); + String[] files = new String[] { WALLPAPER_IMAGE, WALLPAPER_INFO }; + String[] keys = new String[] { WALLPAPER_IMAGE_KEY, WALLPAPER_INFO_KEY }; + if (wallpaper != null) { + try { + final String wallpaperName = wallpaper.getName(); + if (wallpaperName != null && wallpaperName.length() > 0) { + // When the wallpaper has a name, back up the info by itself. + // TODO: Don't rely on the innards of the service object like this! + // TODO: Send a delete for any stored wallpaper image in this case? + files = new String[] { WALLPAPER_INFO }; + keys = new String[] { WALLPAPER_INFO_KEY }; + } + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't get wallpaper name\n" + re); + } + } + addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, files, keys)); + super.onBackup(oldState, data, newState); + } + + @Override + public void onFullBackup(FullBackupDataOutput data) throws IOException { + // At present we back up only the wallpaper + fullWallpaperBackup(data); + } + + private void fullWallpaperBackup(FullBackupDataOutput output) { + // Back up the data files directly. We do them in this specific order -- + // info file followed by image -- because then we need take no special + // steps during restore; the restore will happen properly when the individual + // files are restored piecemeal. + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, + WALLPAPER_INFO_DIR, WALLPAPER_INFO, output.getData()); + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, + WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output.getData()); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + // On restore, we also support a previous data schema "system_files" + addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, + new String[] { WALLPAPER_IMAGE, WALLPAPER_INFO }, + new String[] { WALLPAPER_IMAGE_KEY, WALLPAPER_INFO_KEY} )); + addHelper("system_files", new WallpaperBackupHelper(SystemBackupAgent.this, + new String[] { WALLPAPER_IMAGE }, + new String[] { WALLPAPER_IMAGE_KEY} )); + + try { + super.onRestore(data, appVersionCode, newState); + + IWallpaperManager wallpaper = (IWallpaperManager) ServiceManager.getService( + Context.WALLPAPER_SERVICE); + if (wallpaper != null) { + try { + wallpaper.settingsRestored(); + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't restore settings\n" + re); + } + } + } catch (IOException ex) { + // If there was a failure, delete everything for the wallpaper, this is too aggressive, + // but this is hopefully a rare failure. + Slog.d(TAG, "restore failed", ex); + (new File(WALLPAPER_IMAGE)).delete(); + (new File(WALLPAPER_INFO)).delete(); + } + } + + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + Slog.i(TAG, "Restoring file domain=" + domain + " path=" + path); + + // Bits to indicate postprocessing we may need to perform + boolean restoredWallpaper = false; + + File outFile = null; + // Various domain+files we understand a priori + if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { + if (path.equals(WALLPAPER_INFO_FILENAME)) { + outFile = new File(WALLPAPER_INFO); + restoredWallpaper = true; + } else if (path.equals(WALLPAPER_IMAGE_FILENAME)) { + outFile = new File(WALLPAPER_IMAGE); + restoredWallpaper = true; + } + } + + try { + if (outFile == null) { + Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]"); + } + FullBackup.restoreFile(data, size, type, mode, mtime, outFile); + + if (restoredWallpaper) { + IWallpaperManager wallpaper = + (IWallpaperManager)ServiceManager.getService( + Context.WALLPAPER_SERVICE); + if (wallpaper != null) { + try { + wallpaper.settingsRestored(); + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't restore settings\n" + re); + } + } + } + } catch (IOException e) { + if (restoredWallpaper) { + // Make sure we wind up in a good state + (new File(WALLPAPER_IMAGE)).delete(); + (new File(WALLPAPER_INFO)).delete(); + } + } + } +} diff --git a/services/java/com/android/server/clipboard/ClipboardService.java b/services/java/com/android/server/clipboard/ClipboardService.java new file mode 100644 index 000000000000..6aa596d9d78e --- /dev/null +++ b/services/java/com/android/server/clipboard/ClipboardService.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.clipboard; + +import android.app.ActivityManagerNative; +import android.app.AppGlobals; +import android.app.AppOpsManager; +import android.app.IActivityManager; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.IClipboard; +import android.content.IOnPrimaryClipChangedListener; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.HashSet; + +/** + * Implementation of the clipboard for copy and paste. + */ +public class ClipboardService extends IClipboard.Stub { + + private static final String TAG = "ClipboardService"; + + private final Context mContext; + private final IActivityManager mAm; + private final PackageManager mPm; + private final AppOpsManager mAppOps; + private final IBinder mPermissionOwner; + + private class ListenerInfo { + final int mUid; + final String mPackageName; + ListenerInfo(int uid, String packageName) { + mUid = uid; + mPackageName = packageName; + } + } + + private class PerUserClipboard { + final int userId; + + final RemoteCallbackList primaryClipListeners + = new RemoteCallbackList(); + + ClipData primaryClip; + + final HashSet activePermissionOwners + = new HashSet(); + + PerUserClipboard(int userId) { + this.userId = userId; + } + } + + private SparseArray mClipboards = new SparseArray(); + + /** + * Instantiates the clipboard. + */ + public ClipboardService(Context context) { + mContext = context; + mAm = ActivityManagerNative.getDefault(); + mPm = context.getPackageManager(); + mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); + IBinder permOwner = null; + try { + permOwner = mAm.newUriPermissionOwner("clipboard"); + } catch (RemoteException e) { + Slog.w("clipboard", "AM dead", e); + } + mPermissionOwner = permOwner; + + // Remove the clipboard if a user is removed + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_REMOVED.equals(action)) { + removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } + } + }, userFilter); + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + if (!(e instanceof SecurityException)) { + Slog.wtf("clipboard", "Exception: ", e); + } + throw e; + } + + } + + private PerUserClipboard getClipboard() { + return getClipboard(UserHandle.getCallingUserId()); + } + + private PerUserClipboard getClipboard(int userId) { + synchronized (mClipboards) { + PerUserClipboard puc = mClipboards.get(userId); + if (puc == null) { + puc = new PerUserClipboard(userId); + mClipboards.put(userId, puc); + } + return puc; + } + } + + private void removeClipboard(int userId) { + synchronized (mClipboards) { + mClipboards.remove(userId); + } + } + + public void setPrimaryClip(ClipData clip, String callingPackage) { + synchronized (this) { + if (clip != null && clip.getItemCount() <= 0) { + throw new IllegalArgumentException("No items"); + } + final int callingUid = Binder.getCallingUid(); + if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return; + } + checkDataOwnerLocked(clip, callingUid); + clearActiveOwnersLocked(); + PerUserClipboard clipboard = getClipboard(); + clipboard.primaryClip = clip; + final long ident = Binder.clearCallingIdentity(); + final int n = clipboard.primaryClipListeners.beginBroadcast(); + try { + for (int i = 0; i < n; i++) { + try { + ListenerInfo li = (ListenerInfo) + clipboard.primaryClipListeners.getBroadcastCookie(i); + if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid, + li.mPackageName) == AppOpsManager.MODE_ALLOWED) { + clipboard.primaryClipListeners.getBroadcastItem(i) + .dispatchPrimaryClipChanged(); + } + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + } finally { + clipboard.primaryClipListeners.finishBroadcast(); + Binder.restoreCallingIdentity(ident); + } + } + } + + public ClipData getPrimaryClip(String pkg) { + synchronized (this) { + if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + pkg) != AppOpsManager.MODE_ALLOWED) { + return null; + } + addActiveOwnerLocked(Binder.getCallingUid(), pkg); + return getClipboard().primaryClip; + } + } + + public ClipDescription getPrimaryClipDescription(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return null; + } + PerUserClipboard clipboard = getClipboard(); + return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; + } + } + + public boolean hasPrimaryClip(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + return getClipboard().primaryClip != null; + } + } + + public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage) { + synchronized (this) { + getClipboard().primaryClipListeners.register(listener, + new ListenerInfo(Binder.getCallingUid(), callingPackage)); + } + } + + public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + synchronized (this) { + getClipboard().primaryClipListeners.unregister(listener); + } + } + + public boolean hasClipboardText(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + PerUserClipboard clipboard = getClipboard(); + if (clipboard.primaryClip != null) { + CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); + return text != null && text.length() > 0; + } + return false; + } + } + + private final void checkUriOwnerLocked(Uri uri, int uid) { + if (!"content".equals(uri.getScheme())) { + return; + } + long ident = Binder.clearCallingIdentity(); + try { + // This will throw SecurityException for us. + mAm.checkGrantUriPermission(uid, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private final void checkItemOwnerLocked(ClipData.Item item, int uid) { + if (item.getUri() != null) { + checkUriOwnerLocked(item.getUri(), uid); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + checkUriOwnerLocked(intent.getData(), uid); + } + } + + private final void checkDataOwnerLocked(ClipData data, int uid) { + final int N = data.getItemCount(); + for (int i=0; i mAdminMap + = new HashMap(); + final ArrayList mAdminList + = new ArrayList(); + + public DevicePolicyData(int userHandle) { + mUserHandle = userHandle; + } + } + + final SparseArray mUserData = new SparseArray(); + + Handler mHandler = new Handler(); + + BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + getSendingUserId()); + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || ACTION_EXPIRED_PASSWORD_NOTIFICATION.equals(action)) { + if (DBG) Slog.v(TAG, "Sending password expiration notifications for action " + + action + " for user " + userHandle); + mHandler.post(new Runnable() { + public void run() { + handlePasswordExpirationNotification(getUserData(userHandle)); + } + }); + } + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || KeyChain.ACTION_STORAGE_CHANGED.equals(action)) { + manageMonitoringCertificateNotification(intent); + } + if (Intent.ACTION_USER_REMOVED.equals(action)) { + removeUserData(userHandle); + } else if (Intent.ACTION_USER_STARTED.equals(action) + || Intent.ACTION_PACKAGE_CHANGED.equals(action) + || Intent.ACTION_PACKAGE_REMOVED.equals(action) + || Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + + if (Intent.ACTION_USER_STARTED.equals(action)) { + // Reset the policy data + synchronized (DevicePolicyManagerService.this) { + mUserData.remove(userHandle); + } + } + + handlePackagesChanged(userHandle); + } + } + }; + + static class ActiveAdmin { + final DeviceAdminInfo info; + + int passwordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + + static final int DEF_MINIMUM_PASSWORD_LENGTH = 0; + int minimumPasswordLength = DEF_MINIMUM_PASSWORD_LENGTH; + + static final int DEF_PASSWORD_HISTORY_LENGTH = 0; + int passwordHistoryLength = DEF_PASSWORD_HISTORY_LENGTH; + + static final int DEF_MINIMUM_PASSWORD_UPPER_CASE = 0; + int minimumPasswordUpperCase = DEF_MINIMUM_PASSWORD_UPPER_CASE; + + static final int DEF_MINIMUM_PASSWORD_LOWER_CASE = 0; + int minimumPasswordLowerCase = DEF_MINIMUM_PASSWORD_LOWER_CASE; + + static final int DEF_MINIMUM_PASSWORD_LETTERS = 1; + int minimumPasswordLetters = DEF_MINIMUM_PASSWORD_LETTERS; + + static final int DEF_MINIMUM_PASSWORD_NUMERIC = 1; + int minimumPasswordNumeric = DEF_MINIMUM_PASSWORD_NUMERIC; + + static final int DEF_MINIMUM_PASSWORD_SYMBOLS = 1; + int minimumPasswordSymbols = DEF_MINIMUM_PASSWORD_SYMBOLS; + + static final int DEF_MINIMUM_PASSWORD_NON_LETTER = 0; + int minimumPasswordNonLetter = DEF_MINIMUM_PASSWORD_NON_LETTER; + + static final long DEF_MAXIMUM_TIME_TO_UNLOCK = 0; + long maximumTimeToUnlock = DEF_MAXIMUM_TIME_TO_UNLOCK; + + static final int DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE = 0; + int maximumFailedPasswordsForWipe = DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE; + + static final long DEF_PASSWORD_EXPIRATION_TIMEOUT = 0; + long passwordExpirationTimeout = DEF_PASSWORD_EXPIRATION_TIMEOUT; + + static final long DEF_PASSWORD_EXPIRATION_DATE = 0; + long passwordExpirationDate = DEF_PASSWORD_EXPIRATION_DATE; + + static final int DEF_KEYGUARD_FEATURES_DISABLED = 0; // none + int disabledKeyguardFeatures = DEF_KEYGUARD_FEATURES_DISABLED; + + boolean encryptionRequested = false; + boolean disableCamera = false; + + // TODO: review implementation decisions with frameworks team + boolean specifiesGlobalProxy = false; + String globalProxySpec = null; + String globalProxyExclusionList = null; + + ActiveAdmin(DeviceAdminInfo _info) { + info = _info; + } + + int getUid() { return info.getActivityInfo().applicationInfo.uid; } + + public UserHandle getUserHandle() { + return new UserHandle(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid)); + } + + void writeToXml(XmlSerializer out) + throws IllegalArgumentException, IllegalStateException, IOException { + out.startTag(null, "policies"); + info.writePoliciesToXml(out); + out.endTag(null, "policies"); + if (passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { + out.startTag(null, "password-quality"); + out.attribute(null, "value", Integer.toString(passwordQuality)); + out.endTag(null, "password-quality"); + if (minimumPasswordLength != DEF_MINIMUM_PASSWORD_LENGTH) { + out.startTag(null, "min-password-length"); + out.attribute(null, "value", Integer.toString(minimumPasswordLength)); + out.endTag(null, "min-password-length"); + } + if(passwordHistoryLength != DEF_PASSWORD_HISTORY_LENGTH) { + out.startTag(null, "password-history-length"); + out.attribute(null, "value", Integer.toString(passwordHistoryLength)); + out.endTag(null, "password-history-length"); + } + if (minimumPasswordUpperCase != DEF_MINIMUM_PASSWORD_UPPER_CASE) { + out.startTag(null, "min-password-uppercase"); + out.attribute(null, "value", Integer.toString(minimumPasswordUpperCase)); + out.endTag(null, "min-password-uppercase"); + } + if (minimumPasswordLowerCase != DEF_MINIMUM_PASSWORD_LOWER_CASE) { + out.startTag(null, "min-password-lowercase"); + out.attribute(null, "value", Integer.toString(minimumPasswordLowerCase)); + out.endTag(null, "min-password-lowercase"); + } + if (minimumPasswordLetters != DEF_MINIMUM_PASSWORD_LETTERS) { + out.startTag(null, "min-password-letters"); + out.attribute(null, "value", Integer.toString(minimumPasswordLetters)); + out.endTag(null, "min-password-letters"); + } + if (minimumPasswordNumeric != DEF_MINIMUM_PASSWORD_NUMERIC) { + out.startTag(null, "min-password-numeric"); + out.attribute(null, "value", Integer.toString(minimumPasswordNumeric)); + out.endTag(null, "min-password-numeric"); + } + if (minimumPasswordSymbols != DEF_MINIMUM_PASSWORD_SYMBOLS) { + out.startTag(null, "min-password-symbols"); + out.attribute(null, "value", Integer.toString(minimumPasswordSymbols)); + out.endTag(null, "min-password-symbols"); + } + if (minimumPasswordNonLetter > DEF_MINIMUM_PASSWORD_NON_LETTER) { + out.startTag(null, "min-password-nonletter"); + out.attribute(null, "value", Integer.toString(minimumPasswordNonLetter)); + out.endTag(null, "min-password-nonletter"); + } + } + if (maximumTimeToUnlock != DEF_MAXIMUM_TIME_TO_UNLOCK) { + out.startTag(null, "max-time-to-unlock"); + out.attribute(null, "value", Long.toString(maximumTimeToUnlock)); + out.endTag(null, "max-time-to-unlock"); + } + if (maximumFailedPasswordsForWipe != DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE) { + out.startTag(null, "max-failed-password-wipe"); + out.attribute(null, "value", Integer.toString(maximumFailedPasswordsForWipe)); + out.endTag(null, "max-failed-password-wipe"); + } + if (specifiesGlobalProxy) { + out.startTag(null, "specifies-global-proxy"); + out.attribute(null, "value", Boolean.toString(specifiesGlobalProxy)); + out.endTag(null, "specifies_global_proxy"); + if (globalProxySpec != null) { + out.startTag(null, "global-proxy-spec"); + out.attribute(null, "value", globalProxySpec); + out.endTag(null, "global-proxy-spec"); + } + if (globalProxyExclusionList != null) { + out.startTag(null, "global-proxy-exclusion-list"); + out.attribute(null, "value", globalProxyExclusionList); + out.endTag(null, "global-proxy-exclusion-list"); + } + } + if (passwordExpirationTimeout != DEF_PASSWORD_EXPIRATION_TIMEOUT) { + out.startTag(null, "password-expiration-timeout"); + out.attribute(null, "value", Long.toString(passwordExpirationTimeout)); + out.endTag(null, "password-expiration-timeout"); + } + if (passwordExpirationDate != DEF_PASSWORD_EXPIRATION_DATE) { + out.startTag(null, "password-expiration-date"); + out.attribute(null, "value", Long.toString(passwordExpirationDate)); + out.endTag(null, "password-expiration-date"); + } + if (encryptionRequested) { + out.startTag(null, "encryption-requested"); + out.attribute(null, "value", Boolean.toString(encryptionRequested)); + out.endTag(null, "encryption-requested"); + } + if (disableCamera) { + out.startTag(null, "disable-camera"); + out.attribute(null, "value", Boolean.toString(disableCamera)); + out.endTag(null, "disable-camera"); + } + if (disabledKeyguardFeatures != DEF_KEYGUARD_FEATURES_DISABLED) { + out.startTag(null, "disable-keyguard-features"); + out.attribute(null, "value", Integer.toString(disabledKeyguardFeatures)); + out.endTag(null, "disable-keyguard-features"); + } + } + + void readFromXml(XmlPullParser parser) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + String tag = parser.getName(); + if ("policies".equals(tag)) { + info.readPoliciesFromXml(parser); + } else if ("password-quality".equals(tag)) { + passwordQuality = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-length".equals(tag)) { + minimumPasswordLength = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("password-history-length".equals(tag)) { + passwordHistoryLength = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-uppercase".equals(tag)) { + minimumPasswordUpperCase = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-lowercase".equals(tag)) { + minimumPasswordLowerCase = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-letters".equals(tag)) { + minimumPasswordLetters = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-numeric".equals(tag)) { + minimumPasswordNumeric = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-symbols".equals(tag)) { + minimumPasswordSymbols = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("min-password-nonletter".equals(tag)) { + minimumPasswordNonLetter = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("max-time-to-unlock".equals(tag)) { + maximumTimeToUnlock = Long.parseLong( + parser.getAttributeValue(null, "value")); + } else if ("max-failed-password-wipe".equals(tag)) { + maximumFailedPasswordsForWipe = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else if ("specifies-global-proxy".equals(tag)) { + specifiesGlobalProxy = Boolean.parseBoolean( + parser.getAttributeValue(null, "value")); + } else if ("global-proxy-spec".equals(tag)) { + globalProxySpec = + parser.getAttributeValue(null, "value"); + } else if ("global-proxy-exclusion-list".equals(tag)) { + globalProxyExclusionList = + parser.getAttributeValue(null, "value"); + } else if ("password-expiration-timeout".equals(tag)) { + passwordExpirationTimeout = Long.parseLong( + parser.getAttributeValue(null, "value")); + } else if ("password-expiration-date".equals(tag)) { + passwordExpirationDate = Long.parseLong( + parser.getAttributeValue(null, "value")); + } else if ("encryption-requested".equals(tag)) { + encryptionRequested = Boolean.parseBoolean( + parser.getAttributeValue(null, "value")); + } else if ("disable-camera".equals(tag)) { + disableCamera = Boolean.parseBoolean( + parser.getAttributeValue(null, "value")); + } else if ("disable-keyguard-features".equals(tag)) { + disabledKeyguardFeatures = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } else { + Slog.w(TAG, "Unknown admin tag: " + tag); + } + XmlUtils.skipCurrentTag(parser); + } + } + + void dump(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("uid="); pw.println(getUid()); + pw.print(prefix); pw.println("policies:"); + ArrayList pols = info.getUsedPolicies(); + if (pols != null) { + for (int i=0; i= 0; i--) { + ActiveAdmin aa = policy.mAdminList.get(i); + try { + if (pm.getPackageInfo(aa.info.getPackageName(), 0, userHandle) == null + || pm.getReceiverInfo(aa.info.getComponent(), 0, userHandle) == null) { + removed = true; + policy.mAdminList.remove(i); + policy.mAdminMap.remove(aa.info.getComponent()); + } + } catch (RemoteException re) { + // Shouldn't happen + } + } + if (removed) { + validatePasswordOwnerLocked(policy); + syncDeviceCapabilitiesLocked(policy); + saveSettingsLocked(policy.mUserHandle); + } + } + + /** + * Instantiates the service. + */ + public DevicePolicyManagerService(Context context) { + mContext = context; + mHasFeature = context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_DEVICE_ADMIN); + mWakeLock = ((PowerManager)context.getSystemService(Context.POWER_SERVICE)) + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DPM"); + if (!mHasFeature) { + // Skip the rest of the initialization + return; + } + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BOOT_COMPLETED); + filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION); + filter.addAction(Intent.ACTION_USER_REMOVED); + filter.addAction(Intent.ACTION_USER_STARTED); + filter.addAction(KeyChain.ACTION_STORAGE_CHANGED); + context.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addDataScheme("package"); + context.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); + } + + /** + * Creates and loads the policy data from xml. + * @param userHandle the user for whom to load the policy data + * @return + */ + DevicePolicyData getUserData(int userHandle) { + synchronized (this) { + DevicePolicyData policy = mUserData.get(userHandle); + if (policy == null) { + policy = new DevicePolicyData(userHandle); + mUserData.append(userHandle, policy); + loadSettingsLocked(policy, userHandle); + } + return policy; + } + } + + void removeUserData(int userHandle) { + synchronized (this) { + if (userHandle == UserHandle.USER_OWNER) { + Slog.w(TAG, "Tried to remove device policy file for user 0! Ignoring."); + return; + } + DevicePolicyData policy = mUserData.get(userHandle); + if (policy != null) { + mUserData.remove(userHandle); + } + File policyFile = new File(Environment.getUserSystemDirectory(userHandle), + DEVICE_POLICIES_XML); + policyFile.delete(); + Slog.i(TAG, "Removed device policy file " + policyFile.getAbsolutePath()); + } + } + + void loadDeviceOwner() { + synchronized (this) { + if (DeviceOwner.isRegistered()) { + mDeviceOwner = new DeviceOwner(); + } + } + } + + /** + * Set an alarm for an upcoming event - expiration warning, expiration, or post-expiration + * reminders. Clears alarm if no expirations are configured. + */ + protected void setExpirationAlarmCheckLocked(Context context, DevicePolicyData policy) { + final long expiration = getPasswordExpirationLocked(null, policy.mUserHandle); + final long now = System.currentTimeMillis(); + final long timeToExpire = expiration - now; + final long alarmTime; + if (expiration == 0) { + // No expirations are currently configured: Cancel alarm. + alarmTime = 0; + } else if (timeToExpire <= 0) { + // The password has already expired: Repeat every 24 hours. + alarmTime = now + MS_PER_DAY; + } else { + // Selecting the next alarm time: Roll forward to the next 24 hour multiple before + // the expiration time. + long alarmInterval = timeToExpire % MS_PER_DAY; + if (alarmInterval == 0) { + alarmInterval = MS_PER_DAY; + } + alarmTime = now + alarmInterval; + } + + long token = Binder.clearCallingIdentity(); + try { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pi = PendingIntent.getBroadcastAsUser(context, REQUEST_EXPIRE_PASSWORD, + new Intent(ACTION_EXPIRED_PASSWORD_NOTIFICATION), + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT, + new UserHandle(policy.mUserHandle)); + am.cancel(pi); + if (alarmTime != 0) { + am.set(AlarmManager.RTC, alarmTime, pi); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private IPowerManager getIPowerManager() { + if (mIPowerManager == null) { + IBinder b = ServiceManager.getService(Context.POWER_SERVICE); + mIPowerManager = IPowerManager.Stub.asInterface(b); + } + return mIPowerManager; + } + + private IWindowManager getWindowManager() { + if (mIWindowManager == null) { + IBinder b = ServiceManager.getService(Context.WINDOW_SERVICE); + mIWindowManager = IWindowManager.Stub.asInterface(b); + } + return mIWindowManager; + } + + private NotificationManager getNotificationManager() { + if (mNotificationManager == null) { + mNotificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + return mNotificationManager; + } + + ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle) { + ActiveAdmin admin = getUserData(userHandle).mAdminMap.get(who); + if (admin != null + && who.getPackageName().equals(admin.info.getActivityInfo().packageName) + && who.getClassName().equals(admin.info.getActivityInfo().name)) { + return admin; + } + return null; + } + + ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy) + throws SecurityException { + final int callingUid = Binder.getCallingUid(); + final int userHandle = UserHandle.getUserId(callingUid); + final DevicePolicyData policy = getUserData(userHandle); + if (who != null) { + ActiveAdmin admin = policy.mAdminMap.get(who); + if (admin == null) { + throw new SecurityException("No active admin " + who); + } + if (admin.getUid() != callingUid) { + throw new SecurityException("Admin " + who + " is not owned by uid " + + Binder.getCallingUid()); + } + if (!admin.info.usesPolicy(reqPolicy)) { + throw new SecurityException("Admin " + admin.info.getComponent() + + " did not specify uses-policy for: " + + admin.info.getTagForPolicy(reqPolicy)); + } + return admin; + } else { + final int N = policy.mAdminList.size(); + for (int i=0; i 0) { + for (int i = 0; i < count; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + if (admin.info.usesPolicy(reqPolicy)) { + sendAdminCommandLocked(admin, action); + } + } + } + } + + void removeActiveAdminLocked(final ComponentName adminReceiver, int userHandle) { + final ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle); + if (admin != null) { + sendAdminCommandLocked(admin, + DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLED, + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (DevicePolicyManagerService.this) { + int userHandle = admin.getUserHandle().getIdentifier(); + DevicePolicyData policy = getUserData(userHandle); + boolean doProxyCleanup = admin.info.usesPolicy( + DeviceAdminInfo.USES_POLICY_SETS_GLOBAL_PROXY); + policy.mAdminList.remove(admin); + policy.mAdminMap.remove(adminReceiver); + validatePasswordOwnerLocked(policy); + syncDeviceCapabilitiesLocked(policy); + if (doProxyCleanup) { + resetGlobalProxyLocked(getUserData(userHandle)); + } + saveSettingsLocked(userHandle); + updateMaximumTimeToLockLocked(policy); + } + } + }); + } + } + + public DeviceAdminInfo findAdmin(ComponentName adminName, int userHandle) { + if (!mHasFeature) { + return null; + } + enforceCrossUserPermission(userHandle); + Intent resolveIntent = new Intent(); + resolveIntent.setComponent(adminName); + List infos = mContext.getPackageManager().queryBroadcastReceivers( + resolveIntent, + PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + userHandle); + if (infos == null || infos.size() <= 0) { + throw new IllegalArgumentException("Unknown admin: " + adminName); + } + + try { + return new DeviceAdminInfo(mContext, infos.get(0)); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Bad device admin requested for user=" + userHandle + ": " + adminName, e); + return null; + } catch (IOException e) { + Slog.w(TAG, "Bad device admin requested for user=" + userHandle + ": " + adminName, e); + return null; + } + } + + private static JournaledFile makeJournaledFile(int userHandle) { + final String base = userHandle == 0 + ? "/data/system/" + DEVICE_POLICIES_XML + : new File(Environment.getUserSystemDirectory(userHandle), DEVICE_POLICIES_XML) + .getAbsolutePath(); + return new JournaledFile(new File(base), new File(base + ".tmp")); + } + + private void saveSettingsLocked(int userHandle) { + DevicePolicyData policy = getUserData(userHandle); + JournaledFile journal = makeJournaledFile(userHandle); + FileOutputStream stream = null; + try { + stream = new FileOutputStream(journal.chooseForWrite(), false); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, "utf-8"); + out.startDocument(null, true); + + out.startTag(null, "policies"); + + final int N = policy.mAdminList.size(); + for (int i=0; i= 0) { + out.startTag(null, "password-owner"); + out.attribute(null, "value", Integer.toString(policy.mPasswordOwner)); + out.endTag(null, "password-owner"); + } + + if (policy.mFailedPasswordAttempts != 0) { + out.startTag(null, "failed-password-attempts"); + out.attribute(null, "value", Integer.toString(policy.mFailedPasswordAttempts)); + out.endTag(null, "failed-password-attempts"); + } + + if (policy.mActivePasswordQuality != 0 || policy.mActivePasswordLength != 0 + || policy.mActivePasswordUpperCase != 0 || policy.mActivePasswordLowerCase != 0 + || policy.mActivePasswordLetters != 0 || policy.mActivePasswordNumeric != 0 + || policy.mActivePasswordSymbols != 0 || policy.mActivePasswordNonLetter != 0) { + out.startTag(null, "active-password"); + out.attribute(null, "quality", Integer.toString(policy.mActivePasswordQuality)); + out.attribute(null, "length", Integer.toString(policy.mActivePasswordLength)); + out.attribute(null, "uppercase", Integer.toString(policy.mActivePasswordUpperCase)); + out.attribute(null, "lowercase", Integer.toString(policy.mActivePasswordLowerCase)); + out.attribute(null, "letters", Integer.toString(policy.mActivePasswordLetters)); + out.attribute(null, "numeric", Integer + .toString(policy.mActivePasswordNumeric)); + out.attribute(null, "symbols", Integer.toString(policy.mActivePasswordSymbols)); + out.attribute(null, "nonletter", Integer.toString(policy.mActivePasswordNonLetter)); + out.endTag(null, "active-password"); + } + + out.endTag(null, "policies"); + + out.endDocument(); + stream.close(); + journal.commit(); + sendChangedNotification(userHandle); + } catch (IOException e) { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException ex) { + // Ignore + } + journal.rollback(); + } + } + + private void sendChangedNotification(int userHandle) { + Intent intent = new Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + long ident = Binder.clearCallingIdentity(); + try { + mContext.sendBroadcastAsUser(intent, new UserHandle(userHandle)); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void loadSettingsLocked(DevicePolicyData policy, int userHandle) { + JournaledFile journal = makeJournaledFile(userHandle); + FileInputStream stream = null; + File file = journal.chooseForRead(); + try { + stream = new FileInputStream(file); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, null); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + String tag = parser.getName(); + if (!"policies".equals(tag)) { + throw new XmlPullParserException( + "Settings do not start with policies tag: found " + tag); + } + type = parser.next(); + 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; + } + tag = parser.getName(); + if ("admin".equals(tag)) { + String name = parser.getAttributeValue(null, "name"); + try { + DeviceAdminInfo dai = findAdmin( + ComponentName.unflattenFromString(name), userHandle); + if (DBG && (UserHandle.getUserId(dai.getActivityInfo().applicationInfo.uid) + != userHandle)) { + Slog.w(TAG, "findAdmin returned an incorrect uid " + + dai.getActivityInfo().applicationInfo.uid + " for user " + + userHandle); + } + if (dai != null) { + ActiveAdmin ap = new ActiveAdmin(dai); + ap.readFromXml(parser); + policy.mAdminMap.put(ap.info.getComponent(), ap); + policy.mAdminList.add(ap); + } + } catch (RuntimeException e) { + Slog.w(TAG, "Failed loading admin " + name, e); + } + } else if ("failed-password-attempts".equals(tag)) { + policy.mFailedPasswordAttempts = Integer.parseInt( + parser.getAttributeValue(null, "value")); + XmlUtils.skipCurrentTag(parser); + } else if ("password-owner".equals(tag)) { + policy.mPasswordOwner = Integer.parseInt( + parser.getAttributeValue(null, "value")); + XmlUtils.skipCurrentTag(parser); + } else if ("active-password".equals(tag)) { + policy.mActivePasswordQuality = Integer.parseInt( + parser.getAttributeValue(null, "quality")); + policy.mActivePasswordLength = Integer.parseInt( + parser.getAttributeValue(null, "length")); + policy.mActivePasswordUpperCase = Integer.parseInt( + parser.getAttributeValue(null, "uppercase")); + policy.mActivePasswordLowerCase = Integer.parseInt( + parser.getAttributeValue(null, "lowercase")); + policy.mActivePasswordLetters = Integer.parseInt( + parser.getAttributeValue(null, "letters")); + policy.mActivePasswordNumeric = Integer.parseInt( + parser.getAttributeValue(null, "numeric")); + policy.mActivePasswordSymbols = Integer.parseInt( + parser.getAttributeValue(null, "symbols")); + policy.mActivePasswordNonLetter = Integer.parseInt( + parser.getAttributeValue(null, "nonletter")); + XmlUtils.skipCurrentTag(parser); + } else { + Slog.w(TAG, "Unknown tag: " + tag); + XmlUtils.skipCurrentTag(parser); + } + } + } catch (NullPointerException e) { + Slog.w(TAG, "failed parsing " + file + " " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "failed parsing " + file + " " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "failed parsing " + file + " " + e); + } catch (FileNotFoundException e) { + // Don't be noisy, this is normal if we haven't defined any policies. + } catch (IOException e) { + Slog.w(TAG, "failed parsing " + file + " " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "failed parsing " + file + " " + e); + } + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + // Ignore + } + + // Validate that what we stored for the password quality matches + // sufficiently what is currently set. Note that this is only + // a sanity check in case the two get out of sync; this should + // never normally happen. + LockPatternUtils utils = new LockPatternUtils(mContext); + if (utils.getActivePasswordQuality() < policy.mActivePasswordQuality) { + Slog.w(TAG, "Active password quality 0x" + + Integer.toHexString(policy.mActivePasswordQuality) + + " does not match actual quality 0x" + + Integer.toHexString(utils.getActivePasswordQuality())); + policy.mActivePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + policy.mActivePasswordLength = 0; + policy.mActivePasswordUpperCase = 0; + policy.mActivePasswordLowerCase = 0; + policy.mActivePasswordLetters = 0; + policy.mActivePasswordNumeric = 0; + policy.mActivePasswordSymbols = 0; + policy.mActivePasswordNonLetter = 0; + } + + validatePasswordOwnerLocked(policy); + syncDeviceCapabilitiesLocked(policy); + updateMaximumTimeToLockLocked(policy); + } + + static void validateQualityConstant(int quality) { + switch (quality) { + case DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED: + case DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK: + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + return; + } + throw new IllegalArgumentException("Invalid quality constant: 0x" + + Integer.toHexString(quality)); + } + + void validatePasswordOwnerLocked(DevicePolicyData policy) { + if (policy.mPasswordOwner >= 0) { + boolean haveOwner = false; + for (int i = policy.mAdminList.size() - 1; i >= 0; i--) { + if (policy.mAdminList.get(i).getUid() == policy.mPasswordOwner) { + haveOwner = true; + break; + } + } + if (!haveOwner) { + Slog.w(TAG, "Previous password owner " + policy.mPasswordOwner + + " no longer active; disabling"); + policy.mPasswordOwner = -1; + } + } + } + + /** + * Pushes down policy information to the system for any policies related to general device + * capabilities that need to be enforced by lower level services (e.g. Camera services). + */ + void syncDeviceCapabilitiesLocked(DevicePolicyData policy) { + // Ensure the status of the camera is synced down to the system. Interested native services + // should monitor this value and act accordingly. + boolean systemState = SystemProperties.getBoolean(SYSTEM_PROP_DISABLE_CAMERA, false); + boolean cameraDisabled = getCameraDisabled(null, policy.mUserHandle); + if (cameraDisabled != systemState) { + long token = Binder.clearCallingIdentity(); + try { + String value = cameraDisabled ? "1" : "0"; + if (DBG) Slog.v(TAG, "Change in camera state [" + + SYSTEM_PROP_DISABLE_CAMERA + "] = " + value); + SystemProperties.set(SYSTEM_PROP_DISABLE_CAMERA, value); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + public void systemReady() { + if (!mHasFeature) { + return; + } + synchronized (this) { + loadSettingsLocked(getUserData(UserHandle.USER_OWNER), UserHandle.USER_OWNER); + loadDeviceOwner(); + } + } + + private void handlePasswordExpirationNotification(DevicePolicyData policy) { + synchronized (this) { + final long now = System.currentTimeMillis(); + final int N = policy.mAdminList.size(); + if (N <= 0) { + return; + } + for (int i=0; i < N; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + if (admin.info.usesPolicy(DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) + && admin.passwordExpirationTimeout > 0L + && admin.passwordExpirationDate > 0L + && now >= admin.passwordExpirationDate - EXPIRATION_GRACE_PERIOD_MS) { + sendAdminCommandLocked(admin, DeviceAdminReceiver.ACTION_PASSWORD_EXPIRING); + } + } + setExpirationAlarmCheckLocked(mContext, policy); + } + } + + private void manageMonitoringCertificateNotification(Intent intent) { + final NotificationManager notificationManager = getNotificationManager(); + + final boolean hasCert = DevicePolicyManager.hasAnyCaCertsInstalled(); + if (! hasCert) { + if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { + UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + for (UserInfo user : um.getUsers()) { + notificationManager.cancelAsUser( + null, MONITORING_CERT_NOTIFICATION_ID, user.getUserHandle()); + } + } + return; + } + final boolean isManaged = getDeviceOwner() != null; + int smallIconId; + String contentText; + if (isManaged) { + contentText = mContext.getString(R.string.ssl_ca_cert_noti_managed, + getDeviceOwnerName()); + smallIconId = R.drawable.stat_sys_certificate_info; + } else { + contentText = mContext.getString(R.string.ssl_ca_cert_noti_by_unknown); + smallIconId = android.R.drawable.stat_sys_warning; + } + + Intent dialogIntent = new Intent(Settings.ACTION_MONITORING_CERT_INFO); + dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + dialogIntent.setPackage("com.android.settings"); + // Notification will be sent individually to all users. The activity should start as + // whichever user is current when it starts. + PendingIntent notifyIntent = PendingIntent.getActivityAsUser(mContext, 0, dialogIntent, + PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT); + + Notification noti = new Notification.Builder(mContext) + .setSmallIcon(smallIconId) + .setContentTitle(mContext.getString(R.string.ssl_ca_cert_warning)) + .setContentText(contentText) + .setContentIntent(notifyIntent) + .setPriority(Notification.PRIORITY_HIGH) + .setShowWhen(false) + .build(); + + // If this is a boot intent, this will fire for each user. But if this is a storage changed + // intent, it will fire once, so we need to notify all users. + if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { + UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + for (UserInfo user : um.getUsers()) { + notificationManager.notifyAsUser( + null, MONITORING_CERT_NOTIFICATION_ID, noti, user.getUserHandle()); + } + } else { + notificationManager.notifyAsUser( + null, MONITORING_CERT_NOTIFICATION_ID, noti, UserHandle.CURRENT); + } + } + + /** + * @param adminReceiver The admin to add + * @param refreshing true = update an active admin, no error + */ + public void setActiveAdmin(ComponentName adminReceiver, boolean refreshing, int userHandle) { + if (!mHasFeature) { + return; + } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_DEVICE_ADMINS, null); + enforceCrossUserPermission(userHandle); + + DevicePolicyData policy = getUserData(userHandle); + DeviceAdminInfo info = findAdmin(adminReceiver, userHandle); + if (info == null) { + throw new IllegalArgumentException("Bad admin: " + adminReceiver); + } + synchronized (this) { + long ident = Binder.clearCallingIdentity(); + try { + if (!refreshing && getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null) { + throw new IllegalArgumentException("Admin is already added"); + } + ActiveAdmin newAdmin = new ActiveAdmin(info); + policy.mAdminMap.put(adminReceiver, newAdmin); + int replaceIndex = -1; + if (refreshing) { + final int N = policy.mAdminList.size(); + for (int i=0; i < N; i++) { + ActiveAdmin oldAdmin = policy.mAdminList.get(i); + if (oldAdmin.info.getComponent().equals(adminReceiver)) { + replaceIndex = i; + break; + } + } + } + if (replaceIndex == -1) { + policy.mAdminList.add(newAdmin); + enableIfNecessary(info.getPackageName(), userHandle); + } else { + policy.mAdminList.set(replaceIndex, newAdmin); + } + saveSettingsLocked(userHandle); + sendAdminCommandLocked(newAdmin, DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + public boolean isAdminActive(ComponentName adminReceiver, int userHandle) { + if (!mHasFeature) { + return false; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + return getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null; + } + } + + public boolean hasGrantedPolicy(ComponentName adminReceiver, int policyId, int userHandle) { + if (!mHasFeature) { + return false; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + ActiveAdmin administrator = getActiveAdminUncheckedLocked(adminReceiver, userHandle); + if (administrator == null) { + throw new SecurityException("No active admin " + adminReceiver); + } + return administrator.info.usesPolicy(policyId); + } + } + + @SuppressWarnings("unchecked") + public List getActiveAdmins(int userHandle) { + if (!mHasFeature) { + return Collections.EMPTY_LIST; + } + + enforceCrossUserPermission(userHandle); + synchronized (this) { + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + if (N <= 0) { + return null; + } + ArrayList res = new ArrayList(N); + for (int i=0; i= 0 ms"); + } + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD); + // Calling this API automatically bumps the expiration date + final long expiration = timeout > 0L ? (timeout + System.currentTimeMillis()) : 0L; + ap.passwordExpirationDate = expiration; + ap.passwordExpirationTimeout = timeout; + if (timeout > 0L) { + Slog.w(TAG, "setPasswordExpiration(): password will expire on " + + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT) + .format(new Date(expiration))); + } + saveSettingsLocked(userHandle); + // in case this is the first one + setExpirationAlarmCheckLocked(mContext, getUserData(userHandle)); + } + } + + /** + * Return a single admin's expiration cycle time, or the min of all cycle times. + * Returns 0 if not configured. + */ + public long getPasswordExpirationTimeout(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0L; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return admin != null ? admin.passwordExpirationTimeout : 0L; + } + + long timeout = 0L; + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + if (timeout == 0L || (admin.passwordExpirationTimeout != 0L + && timeout > admin.passwordExpirationTimeout)) { + timeout = admin.passwordExpirationTimeout; + } + } + return timeout; + } + } + + /** + * Return a single admin's expiration date/time, or the min (soonest) for all admins. + * Returns 0 if not configured. + */ + private long getPasswordExpirationLocked(ComponentName who, int userHandle) { + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return admin != null ? admin.passwordExpirationDate : 0L; + } + + long timeout = 0L; + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + if (timeout == 0L || (admin.passwordExpirationDate != 0 + && timeout > admin.passwordExpirationDate)) { + timeout = admin.passwordExpirationDate; + } + } + return timeout; + } + + public long getPasswordExpiration(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0L; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + return getPasswordExpirationLocked(who, userHandle); + } + } + + public void setPasswordMinimumUpperCase(ComponentName who, int length, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD); + if (ap.minimumPasswordUpperCase != length) { + ap.minimumPasswordUpperCase = length; + saveSettingsLocked(userHandle); + } + } + } + + public int getPasswordMinimumUpperCase(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + int length = 0; + + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return admin != null ? admin.minimumPasswordUpperCase : length; + } + + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + for (int i=0; i= getPasswordMinimumUpperCase(null, userHandle) + && policy.mActivePasswordLowerCase >= getPasswordMinimumLowerCase(null, userHandle) + && policy.mActivePasswordLetters >= getPasswordMinimumLetters(null, userHandle) + && policy.mActivePasswordNumeric >= getPasswordMinimumNumeric(null, userHandle) + && policy.mActivePasswordSymbols >= getPasswordMinimumSymbols(null, userHandle) + && policy.mActivePasswordNonLetter >= getPasswordMinimumNonLetter(null, userHandle); + } + } + + public int getCurrentFailedPasswordAttempts(int userHandle) { + enforceCrossUserPermission(userHandle); + synchronized (this) { + // This API can only be called by an active device admin, + // so try to retrieve it to check that the caller is one. + getActiveAdminForCallerLocked(null, + DeviceAdminInfo.USES_POLICY_WATCH_LOGIN); + return getUserData(userHandle).mFailedPasswordAttempts; + } + } + + public void setMaximumFailedPasswordsForWipe(ComponentName who, int num, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + // This API can only be called by an active device admin, + // so try to retrieve it to check that the caller is one. + getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_WIPE_DATA); + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_WATCH_LOGIN); + if (ap.maximumFailedPasswordsForWipe != num) { + ap.maximumFailedPasswordsForWipe = num; + saveSettingsLocked(userHandle); + } + } + } + + public int getMaximumFailedPasswordsForWipe(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + DevicePolicyData policy = getUserData(userHandle); + int count = 0; + + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return admin != null ? admin.maximumFailedPasswordsForWipe : count; + } + + final int N = policy.mAdminList.size(); + for (int i=0; i admin.maximumFailedPasswordsForWipe) { + count = admin.maximumFailedPasswordsForWipe; + } + } + return count; + } + } + + public boolean resetPassword(String password, int flags, int userHandle) { + if (!mHasFeature) { + return false; + } + enforceCrossUserPermission(userHandle); + int quality; + synchronized (this) { + // This API can only be called by an active device admin, + // so try to retrieve it to check that the caller is one. + getActiveAdminForCallerLocked(null, + DeviceAdminInfo.USES_POLICY_RESET_PASSWORD); + quality = getPasswordQuality(null, userHandle); + if (quality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { + int realQuality = LockPatternUtils.computePasswordQuality(password); + if (realQuality < quality + && quality != DevicePolicyManager.PASSWORD_QUALITY_COMPLEX) { + Slog.w(TAG, "resetPassword: password quality 0x" + + Integer.toHexString(realQuality) + + " does not meet required quality 0x" + + Integer.toHexString(quality)); + return false; + } + quality = Math.max(realQuality, quality); + } + int length = getPasswordMinimumLength(null, userHandle); + if (password.length() < length) { + Slog.w(TAG, "resetPassword: password length " + password.length() + + " does not meet required length " + length); + return false; + } + if (quality == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX) { + int letters = 0; + int uppercase = 0; + int lowercase = 0; + int numbers = 0; + int symbols = 0; + int nonletter = 0; + for (int i = 0; i < password.length(); i++) { + char c = password.charAt(i); + if (c >= 'A' && c <= 'Z') { + letters++; + uppercase++; + } else if (c >= 'a' && c <= 'z') { + letters++; + lowercase++; + } else if (c >= '0' && c <= '9') { + numbers++; + nonletter++; + } else { + symbols++; + nonletter++; + } + } + int neededLetters = getPasswordMinimumLetters(null, userHandle); + if(letters < neededLetters) { + Slog.w(TAG, "resetPassword: number of letters " + letters + + " does not meet required number of letters " + neededLetters); + return false; + } + int neededNumbers = getPasswordMinimumNumeric(null, userHandle); + if (numbers < neededNumbers) { + Slog.w(TAG, "resetPassword: number of numerical digits " + numbers + + " does not meet required number of numerical digits " + + neededNumbers); + return false; + } + int neededLowerCase = getPasswordMinimumLowerCase(null, userHandle); + if (lowercase < neededLowerCase) { + Slog.w(TAG, "resetPassword: number of lowercase letters " + lowercase + + " does not meet required number of lowercase letters " + + neededLowerCase); + return false; + } + int neededUpperCase = getPasswordMinimumUpperCase(null, userHandle); + if (uppercase < neededUpperCase) { + Slog.w(TAG, "resetPassword: number of uppercase letters " + uppercase + + " does not meet required number of uppercase letters " + + neededUpperCase); + return false; + } + int neededSymbols = getPasswordMinimumSymbols(null, userHandle); + if (symbols < neededSymbols) { + Slog.w(TAG, "resetPassword: number of special symbols " + symbols + + " does not meet required number of special symbols " + neededSymbols); + return false; + } + int neededNonLetter = getPasswordMinimumNonLetter(null, userHandle); + if (nonletter < neededNonLetter) { + Slog.w(TAG, "resetPassword: number of non-letter characters " + nonletter + + " does not meet required number of non-letter characters " + + neededNonLetter); + return false; + } + } + } + + int callingUid = Binder.getCallingUid(); + DevicePolicyData policy = getUserData(userHandle); + if (policy.mPasswordOwner >= 0 && policy.mPasswordOwner != callingUid) { + Slog.w(TAG, "resetPassword: already set by another uid and not entered by user"); + return false; + } + + // Don't do this with the lock held, because it is going to call + // back in to the service. + long ident = Binder.clearCallingIdentity(); + try { + LockPatternUtils utils = new LockPatternUtils(mContext); + utils.saveLockPassword(password, quality, false, userHandle); + synchronized (this) { + int newOwner = (flags&DevicePolicyManager.RESET_PASSWORD_REQUIRE_ENTRY) + != 0 ? callingUid : -1; + if (policy.mPasswordOwner != newOwner) { + policy.mPasswordOwner = newOwner; + saveSettingsLocked(userHandle); + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + return true; + } + + public void setMaximumTimeToLock(ComponentName who, long timeMs, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_FORCE_LOCK); + if (ap.maximumTimeToUnlock != timeMs) { + ap.maximumTimeToUnlock = timeMs; + saveSettingsLocked(userHandle); + updateMaximumTimeToLockLocked(getUserData(userHandle)); + } + } + } + + void updateMaximumTimeToLockLocked(DevicePolicyData policy) { + long timeMs = getMaximumTimeToLock(null, policy.mUserHandle); + if (policy.mLastMaximumTimeToLock == timeMs) { + return; + } + + long ident = Binder.clearCallingIdentity(); + try { + if (timeMs <= 0) { + timeMs = Integer.MAX_VALUE; + } else { + // Make sure KEEP_SCREEN_ON is disabled, since that + // would allow bypassing of the maximum time to lock. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0); + } + + policy.mLastMaximumTimeToLock = timeMs; + + try { + getIPowerManager().setMaximumScreenOffTimeoutFromDeviceAdmin((int)timeMs); + } catch (RemoteException e) { + Slog.w(TAG, "Failure talking with power manager", e); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + public long getMaximumTimeToLock(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + long time = 0; + + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return admin != null ? admin.maximumTimeToUnlock : time; + } + + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + for (int i=0; i admin.maximumTimeToUnlock) { + time = admin.maximumTimeToUnlock; + } + } + return time; + } + } + + public void lockNow() { + if (!mHasFeature) { + return; + } + synchronized (this) { + // This API can only be called by an active device admin, + // so try to retrieve it to check that the caller is one. + getActiveAdminForCallerLocked(null, + DeviceAdminInfo.USES_POLICY_FORCE_LOCK); + lockNowUnchecked(); + } + } + + private void lockNowUnchecked() { + long ident = Binder.clearCallingIdentity(); + try { + // Power off the display + getIPowerManager().goToSleep(SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN); + // Ensure the device is locked + getWindowManager().lockNow(null); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private boolean isExtStorageEncrypted() { + String state = SystemProperties.get("vold.decrypt"); + return !"".equals(state); + } + + public boolean installCaCert(byte[] certBuffer) throws RemoteException { + mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null); + KeyChainConnection keyChainConnection = null; + byte[] pemCert; + try { + X509Certificate cert = parseCert(certBuffer); + pemCert = Credentials.convertToPem(cert); + } catch (CertificateException ce) { + Log.e(TAG, "Problem converting cert", ce); + return false; + } catch (IOException ioe) { + Log.e(TAG, "Problem reading cert", ioe); + return false; + } + try { + keyChainConnection = KeyChain.bind(mContext); + try { + keyChainConnection.getService().installCaCertificate(pemCert); + return true; + } finally { + if (keyChainConnection != null) { + keyChainConnection.close(); + keyChainConnection = null; + } + } + } catch (InterruptedException e1) { + Log.w(TAG, "installCaCertsToKeyChain(): ", e1); + Thread.currentThread().interrupt(); + } + return false; + } + + private static X509Certificate parseCert(byte[] certBuffer) + throws CertificateException, IOException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream( + certBuffer)); + } + + public void uninstallCaCert(final byte[] certBuffer) { + mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null); + TrustedCertificateStore certStore = new TrustedCertificateStore(); + String alias = null; + try { + X509Certificate cert = parseCert(certBuffer); + alias = certStore.getCertificateAlias(cert); + } catch (CertificateException ce) { + Log.e(TAG, "Problem creating X509Certificate", ce); + return; + } catch (IOException ioe) { + Log.e(TAG, "Problem reading certificate", ioe); + return; + } + try { + KeyChainConnection keyChainConnection = KeyChain.bind(mContext); + IKeyChainService service = keyChainConnection.getService(); + try { + service.deleteCaCertificate(alias); + } catch (RemoteException e) { + Log.e(TAG, "from CaCertUninstaller: ", e); + } finally { + keyChainConnection.close(); + keyChainConnection = null; + } + } catch (InterruptedException ie) { + Log.w(TAG, "CaCertUninstaller: ", ie); + Thread.currentThread().interrupt(); + } + } + + void wipeDataLocked(int flags) { + // If the SD card is encrypted and non-removable, we have to force a wipe. + boolean forceExtWipe = !Environment.isExternalStorageRemovable() && isExtStorageEncrypted(); + boolean wipeExtRequested = (flags&DevicePolicyManager.WIPE_EXTERNAL_STORAGE) != 0; + + // Note: we can only do the wipe via ExternalStorageFormatter if the volume is not emulated. + if ((forceExtWipe || wipeExtRequested) && !Environment.isExternalStorageEmulated()) { + Intent intent = new Intent(ExternalStorageFormatter.FORMAT_AND_FACTORY_RESET); + intent.putExtra(ExternalStorageFormatter.EXTRA_ALWAYS_RESET, true); + intent.setComponent(ExternalStorageFormatter.COMPONENT_NAME); + mWakeLock.acquire(10000); + mContext.startService(intent); + } else { + try { + RecoverySystem.rebootWipeUserData(mContext); + } catch (IOException e) { + Slog.w(TAG, "Failed requesting data wipe", e); + } + } + } + + public void wipeData(int flags, final int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + // This API can only be called by an active device admin, + // so try to retrieve it to check that the caller is one. + getActiveAdminForCallerLocked(null, + DeviceAdminInfo.USES_POLICY_WIPE_DATA); + long ident = Binder.clearCallingIdentity(); + try { + wipeDeviceOrUserLocked(flags, userHandle); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + private void wipeDeviceOrUserLocked(int flags, final int userHandle) { + if (userHandle == UserHandle.USER_OWNER) { + wipeDataLocked(flags); + } else { + lockNowUnchecked(); + mHandler.post(new Runnable() { + public void run() { + try { + ActivityManagerNative.getDefault().switchUser(UserHandle.USER_OWNER); + ((UserManager) mContext.getSystemService(Context.USER_SERVICE)) + .removeUser(userHandle); + } catch (RemoteException re) { + // Shouldn't happen + } + } + }); + } + } + + public void getRemoveWarning(ComponentName comp, final RemoteCallback result, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.BIND_DEVICE_ADMIN, null); + + synchronized (this) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(comp, userHandle); + if (admin == null) { + try { + result.sendResult(null); + } catch (RemoteException e) { + } + return; + } + Intent intent = new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLE_REQUESTED); + intent.setComponent(admin.info.getComponent()); + mContext.sendOrderedBroadcastAsUser(intent, new UserHandle(userHandle), + null, new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + try { + result.sendResult(getResultExtras(false)); + } catch (RemoteException e) { + } + } + }, null, Activity.RESULT_OK, null, null); + } + } + + public void setActivePasswordState(int quality, int length, int letters, int uppercase, + int lowercase, int numbers, int symbols, int nonletter, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.BIND_DEVICE_ADMIN, null); + DevicePolicyData p = getUserData(userHandle); + + validateQualityConstant(quality); + + synchronized (this) { + if (p.mActivePasswordQuality != quality || p.mActivePasswordLength != length + || p.mFailedPasswordAttempts != 0 || p.mActivePasswordLetters != letters + || p.mActivePasswordUpperCase != uppercase + || p.mActivePasswordLowerCase != lowercase || p.mActivePasswordNumeric != numbers + || p.mActivePasswordSymbols != symbols || p.mActivePasswordNonLetter != nonletter) { + long ident = Binder.clearCallingIdentity(); + try { + p.mActivePasswordQuality = quality; + p.mActivePasswordLength = length; + p.mActivePasswordLetters = letters; + p.mActivePasswordLowerCase = lowercase; + p.mActivePasswordUpperCase = uppercase; + p.mActivePasswordNumeric = numbers; + p.mActivePasswordSymbols = symbols; + p.mActivePasswordNonLetter = nonletter; + p.mFailedPasswordAttempts = 0; + saveSettingsLocked(userHandle); + updatePasswordExpirationsLocked(userHandle); + setExpirationAlarmCheckLocked(mContext, p); + sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED, + DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, userHandle); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + } + + /** + * Called any time the device password is updated. Resets all password expiration clocks. + */ + private void updatePasswordExpirationsLocked(int userHandle) { + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + if (N > 0) { + for (int i=0; i 0L ? (timeout + System.currentTimeMillis()) : 0L; + admin.passwordExpirationDate = expiration; + } + } + saveSettingsLocked(userHandle); + } + } + + public void reportFailedPasswordAttempt(int userHandle) { + enforceCrossUserPermission(userHandle); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.BIND_DEVICE_ADMIN, null); + + synchronized (this) { + DevicePolicyData policy = getUserData(userHandle); + long ident = Binder.clearCallingIdentity(); + try { + policy.mFailedPasswordAttempts++; + saveSettingsLocked(userHandle); + if (mHasFeature) { + int max = getMaximumFailedPasswordsForWipe(null, userHandle); + if (max > 0 && policy.mFailedPasswordAttempts >= max) { + wipeDeviceOrUserLocked(0, userHandle); + } + sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_FAILED, + DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + public void reportSuccessfulPasswordAttempt(int userHandle) { + enforceCrossUserPermission(userHandle); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.BIND_DEVICE_ADMIN, null); + + synchronized (this) { + DevicePolicyData policy = getUserData(userHandle); + if (policy.mFailedPasswordAttempts != 0 || policy.mPasswordOwner >= 0) { + long ident = Binder.clearCallingIdentity(); + try { + policy.mFailedPasswordAttempts = 0; + policy.mPasswordOwner = -1; + saveSettingsLocked(userHandle); + if (mHasFeature) { + sendAdminCommandLocked(DeviceAdminReceiver.ACTION_PASSWORD_SUCCEEDED, + DeviceAdminInfo.USES_POLICY_WATCH_LOGIN, userHandle); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + } + + public ComponentName setGlobalProxy(ComponentName who, String proxySpec, + String exclusionList, int userHandle) { + if (!mHasFeature) { + return null; + } + enforceCrossUserPermission(userHandle); + synchronized(this) { + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + + // Only check if owner has set global proxy. We don't allow other users to set it. + DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); + ActiveAdmin admin = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_SETS_GLOBAL_PROXY); + + // Scan through active admins and find if anyone has already + // set the global proxy. + Set compSet = policy.mAdminMap.keySet(); + for (ComponentName component : compSet) { + ActiveAdmin ap = policy.mAdminMap.get(component); + if ((ap.specifiesGlobalProxy) && (!component.equals(who))) { + // Another admin already sets the global proxy + // Return it to the caller. + return component; + } + } + + // If the user is not the owner, don't set the global proxy. Fail silently. + if (UserHandle.getCallingUserId() != UserHandle.USER_OWNER) { + Slog.w(TAG, "Only the owner is allowed to set the global proxy. User " + + userHandle + " is not permitted."); + return null; + } + if (proxySpec == null) { + admin.specifiesGlobalProxy = false; + admin.globalProxySpec = null; + admin.globalProxyExclusionList = null; + } else { + + admin.specifiesGlobalProxy = true; + admin.globalProxySpec = proxySpec; + admin.globalProxyExclusionList = exclusionList; + } + + // Reset the global proxy accordingly + // Do this using system permissions, as apps cannot write to secure settings + long origId = Binder.clearCallingIdentity(); + resetGlobalProxyLocked(policy); + Binder.restoreCallingIdentity(origId); + return null; + } + } + + public ComponentName getGlobalProxyAdmin(int userHandle) { + if (!mHasFeature) { + return null; + } + enforceCrossUserPermission(userHandle); + synchronized(this) { + DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); + // Scan through active admins and find if anyone has already + // set the global proxy. + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + ActiveAdmin ap = policy.mAdminList.get(i); + if (ap.specifiesGlobalProxy) { + // Device admin sets the global proxy + // Return it to the caller. + return ap.info.getComponent(); + } + } + } + // No device admin sets the global proxy. + return null; + } + + private void resetGlobalProxyLocked(DevicePolicyData policy) { + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + ActiveAdmin ap = policy.mAdminList.get(i); + if (ap.specifiesGlobalProxy) { + saveGlobalProxyLocked(ap.globalProxySpec, ap.globalProxyExclusionList); + return; + } + } + // No device admins defining global proxies - reset global proxy settings to none + saveGlobalProxyLocked(null, null); + } + + private void saveGlobalProxyLocked(String proxySpec, String exclusionList) { + if (exclusionList == null) { + exclusionList = ""; + } + if (proxySpec == null) { + proxySpec = ""; + } + // Remove white spaces + proxySpec = proxySpec.trim(); + String data[] = proxySpec.split(":"); + int proxyPort = 8080; + if (data.length > 1) { + try { + proxyPort = Integer.parseInt(data[1]); + } catch (NumberFormatException e) {} + } + exclusionList = exclusionList.trim(); + ContentResolver res = mContext.getContentResolver(); + + ProxyProperties proxyProperties = new ProxyProperties(data[0], proxyPort, exclusionList); + if (!proxyProperties.isValid()) { + Slog.e(TAG, "Invalid proxy properties, ignoring: " + proxyProperties.toString()); + return; + } + Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST, data[0]); + Settings.Global.putInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, proxyPort); + Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST, + exclusionList); + } + + /** + * Set the storage encryption request for a single admin. Returns the new total request + * status (for all admins). + */ + public int setStorageEncryption(ComponentName who, boolean encrypt, int userHandle) { + if (!mHasFeature) { + return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + // Check for permissions + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + // Only owner can set storage encryption + if (userHandle != UserHandle.USER_OWNER + || UserHandle.getCallingUserId() != UserHandle.USER_OWNER) { + Slog.w(TAG, "Only owner is allowed to set storage encryption. User " + + UserHandle.getCallingUserId() + " is not permitted."); + return 0; + } + + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_ENCRYPTED_STORAGE); + + // Quick exit: If the filesystem does not support encryption, we can exit early. + if (!isEncryptionSupported()) { + return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; + } + + // (1) Record the value for the admin so it's sticky + if (ap.encryptionRequested != encrypt) { + ap.encryptionRequested = encrypt; + saveSettingsLocked(userHandle); + } + + DevicePolicyData policy = getUserData(UserHandle.USER_OWNER); + // (2) Compute "max" for all admins + boolean newRequested = false; + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + newRequested |= policy.mAdminList.get(i).encryptionRequested; + } + + // Notify OS of new request + setEncryptionRequested(newRequested); + + // Return the new global request status + return newRequested + ? DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE + : DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE; + } + } + + /** + * Get the current storage encryption request status for a given admin, or aggregate of all + * active admins. + */ + public boolean getStorageEncryption(ComponentName who, int userHandle) { + if (!mHasFeature) { + return false; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + // Check for permissions if a particular caller is specified + if (who != null) { + // When checking for a single caller, status is based on caller's request + ActiveAdmin ap = getActiveAdminUncheckedLocked(who, userHandle); + return ap != null ? ap.encryptionRequested : false; + } + + // If no particular caller is specified, return the aggregate set of requests. + // This is short circuited by returning true on the first hit. + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + if (policy.mAdminList.get(i).encryptionRequested) { + return true; + } + } + return false; + } + } + + /** + * Get the current encryption status of the device. + */ + public int getStorageEncryptionStatus(int userHandle) { + if (!mHasFeature) { + // Ok to return current status. + } + enforceCrossUserPermission(userHandle); + return getEncryptionStatus(); + } + + /** + * Hook to low-levels: This should report if the filesystem supports encrypted storage. + */ + private boolean isEncryptionSupported() { + // Note, this can be implemented as + // return getEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; + // But is provided as a separate internal method if there's a faster way to do a + // simple check for supported-or-not. + return getEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; + } + + /** + * Hook to low-levels: Reporting the current status of encryption. + * @return A value such as {@link DevicePolicyManager#ENCRYPTION_STATUS_UNSUPPORTED} or + * {@link DevicePolicyManager#ENCRYPTION_STATUS_INACTIVE} or + * {@link DevicePolicyManager#ENCRYPTION_STATUS_ACTIVE}. + */ + private int getEncryptionStatus() { + String status = SystemProperties.get("ro.crypto.state", "unsupported"); + if ("encrypted".equalsIgnoreCase(status)) { + return DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE; + } else if ("unencrypted".equalsIgnoreCase(status)) { + return DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE; + } else { + return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; + } + } + + /** + * Hook to low-levels: If needed, record the new admin setting for encryption. + */ + private void setEncryptionRequested(boolean encrypt) { + } + + /** + * The system property used to share the state of the camera. The native camera service + * is expected to read this property and act accordingly. + */ + public static final String SYSTEM_PROP_DISABLE_CAMERA = "sys.secpolicy.camera.disabled"; + + /** + * Disables all device cameras according to the specified admin. + */ + public void setCameraDisabled(ComponentName who, boolean disabled, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA); + if (ap.disableCamera != disabled) { + ap.disableCamera = disabled; + saveSettingsLocked(userHandle); + } + syncDeviceCapabilitiesLocked(getUserData(userHandle)); + } + } + + /** + * Gets whether or not all device cameras are disabled for a given admin, or disabled for any + * active admins. + */ + public boolean getCameraDisabled(ComponentName who, int userHandle) { + if (!mHasFeature) { + return false; + } + synchronized (this) { + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return (admin != null) ? admin.disableCamera : false; + } + + DevicePolicyData policy = getUserData(userHandle); + // Determine whether or not the device camera is disabled for any active admins. + final int N = policy.mAdminList.size(); + for (int i = 0; i < N; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + if (admin.disableCamera) { + return true; + } + } + return false; + } + } + + /** + * Selectively disable keyguard features. + */ + public void setKeyguardDisabledFeatures(ComponentName who, int which, int userHandle) { + if (!mHasFeature) { + return; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who == null) { + throw new NullPointerException("ComponentName is null"); + } + ActiveAdmin ap = getActiveAdminForCallerLocked(who, + DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES); + if (ap.disabledKeyguardFeatures != which) { + ap.disabledKeyguardFeatures = which; + saveSettingsLocked(userHandle); + } + syncDeviceCapabilitiesLocked(getUserData(userHandle)); + } + } + + /** + * Gets the disabled state for features in keyguard for the given admin, + * or the aggregate of all active admins if who is null. + */ + public int getKeyguardDisabledFeatures(ComponentName who, int userHandle) { + if (!mHasFeature) { + return 0; + } + enforceCrossUserPermission(userHandle); + synchronized (this) { + if (who != null) { + ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle); + return (admin != null) ? admin.disabledKeyguardFeatures : 0; + } + + // Determine which keyguard features are disabled for any active admins. + DevicePolicyData policy = getUserData(userHandle); + final int N = policy.mAdminList.size(); + int which = 0; + for (int i = 0; i < N; i++) { + ActiveAdmin admin = policy.mAdminList.get(i); + which |= admin.disabledKeyguardFeatures; + } + return which; + } + } + + @Override + public boolean setDeviceOwner(String packageName, String ownerName) { + if (!mHasFeature) { + return false; + } + if (packageName == null + || !DeviceOwner.isInstalled(packageName, mContext.getPackageManager())) { + throw new IllegalArgumentException("Invalid package name " + packageName + + " for device owner"); + } + synchronized (this) { + if (mDeviceOwner == null && !isDeviceProvisioned()) { + mDeviceOwner = new DeviceOwner(packageName, ownerName); + mDeviceOwner.writeOwnerFile(); + return true; + } else { + throw new IllegalStateException("Trying to set device owner to " + packageName + + ", owner=" + mDeviceOwner.getPackageName() + + ", device_provisioned=" + isDeviceProvisioned()); + } + } + } + + @Override + public boolean isDeviceOwner(String packageName) { + if (!mHasFeature) { + return false; + } + synchronized (this) { + return mDeviceOwner != null + && mDeviceOwner.getPackageName().equals(packageName); + } + } + + @Override + public String getDeviceOwner() { + if (!mHasFeature) { + return null; + } + synchronized (this) { + if (mDeviceOwner != null) { + return mDeviceOwner.getPackageName(); + } + } + return null; + } + + @Override + public String getDeviceOwnerName() { + if (!mHasFeature) { + return null; + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USERS, null); + synchronized (this) { + if (mDeviceOwner != null) { + return mDeviceOwner.getName(); + } + } + return null; + } + + private boolean isDeviceProvisioned() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) > 0; + } + + private void enforceCrossUserPermission(int userHandle) { + if (userHandle < 0) { + throw new IllegalArgumentException("Invalid userId " + userHandle); + } + final int callingUid = Binder.getCallingUid(); + if (userHandle == UserHandle.getUserId(callingUid)) return; + if (callingUid != Process.SYSTEM_UID && callingUid != 0) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "Must be system or have" + + " INTERACT_ACROSS_USERS_FULL permission"); + } + } + + private void enableIfNecessary(String packageName, int userId) { + try { + IPackageManager ipm = AppGlobals.getPackageManager(); + ApplicationInfo ai = ipm.getApplicationInfo(packageName, + PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + userId); + if (ai.enabledSetting + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { + ipm.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, userId, "DevicePolicyManager"); + } + } catch (RemoteException e) { + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + pw.println("Permission Denial: can't dump DevicePolicyManagerService from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + final Printer p = new PrintWriterPrinter(pw); + + synchronized (this) { + p.println("Current Device Policy Manager state:"); + + int userCount = mUserData.size(); + for (int u = 0; u < userCount; u++) { + DevicePolicyData policy = getUserData(mUserData.keyAt(u)); + p.println(" Enabled Device Admins (User " + policy.mUserHandle + "):"); + final int N = policy.mAdminList.size(); + for (int i=0; i 0 ? new String[setCount] : null; + String[] myClasses = setCount > 0 ? new String[setCount] : null; + String[] myComponents = setCount > 0 ? new String[setCount] : null; + + int setPos = 0; + + int outerDepth = parser.getDepth(); + int type; + 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(); + //Log.i(TAG, "Parse outerDepth=" + outerDepth + " depth=" + // + parser.getDepth() + " tag=" + tagName); + if (tagName.equals(TAG_SET)) { + String name = parser.getAttributeValue(null, ATTR_NAME); + if (name == null) { + if (mParseError == null) { + mParseError = "No name in set tag in preferred activity " + + mShortComponent; + } + } else if (setPos >= setCount) { + if (mParseError == null) { + mParseError = "Too many set tags in preferred activity " + + mShortComponent; + } + } else { + ComponentName cn = ComponentName.unflattenFromString(name); + if (cn == null) { + if (mParseError == null) { + mParseError = "Bad set name " + name + " in preferred activity " + + mShortComponent; + } + } else { + myPackages[setPos] = cn.getPackageName(); + myClasses[setPos] = cn.getClassName(); + myComponents[setPos] = name; + setPos++; + } + } + XmlUtils.skipCurrentTag(parser); + } else if (!mCallbacks.onReadTag(tagName, parser)) { + Slog.w("PreferredComponent", "Unknown element: " + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + + if (setPos != setCount) { + if (mParseError == null) { + mParseError = "Not enough set tags (expected " + setCount + + " but found " + setPos + ") in " + mShortComponent; + } + } + + mSetPackages = myPackages; + mSetClasses = myClasses; + mSetComponents = myComponents; + } + + public String getParseError() { + return mParseError; + } + + public void writeToXml(XmlSerializer serializer, boolean full) throws IOException { + final int NS = mSetClasses != null ? mSetClasses.length : 0; + serializer.attribute(null, ATTR_NAME, mShortComponent); + if (full) { + if (mMatch != 0) { + serializer.attribute(null, ATTR_MATCH, Integer.toHexString(mMatch)); + } + serializer.attribute(null, ATTR_ALWAYS, Boolean.toString(mAlways)); + serializer.attribute(null, ATTR_SET, Integer.toString(NS)); + for (int s=0; s query, int priority) { + if (mSetPackages == null) return false; + final int NQ = query.size(); + final int NS = mSetPackages.length; + int numMatch = 0; + for (int i=0; i mWallpaperMap = new SparseArray(); + + int mCurrentUserId; + + static class WallpaperData { + + int userId; + + File wallpaperFile; + + /** + * Client is currently writing a new image wallpaper. + */ + boolean imageWallpaperPending; + + /** + * Resource name if using a picture from the wallpaper gallery + */ + String name = ""; + + /** + * The component name of the currently set live wallpaper. + */ + ComponentName wallpaperComponent; + + /** + * The component name of the wallpaper that should be set next. + */ + ComponentName nextWallpaperComponent; + + WallpaperConnection connection; + long lastDiedTime; + boolean wallpaperUpdating; + WallpaperObserver wallpaperObserver; + + /** + * List of callbacks registered they should each be notified when the wallpaper is changed. + */ + private RemoteCallbackList callbacks + = new RemoteCallbackList(); + + int width = -1; + int height = -1; + + WallpaperData(int userId) { + this.userId = userId; + wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER); + } + } + + class WallpaperConnection extends IWallpaperConnection.Stub + implements ServiceConnection { + final WallpaperInfo mInfo; + final Binder mToken = new Binder(); + IWallpaperService mService; + IWallpaperEngine mEngine; + WallpaperData mWallpaper; + IRemoteCallback mReply; + + boolean mDimensionsChanged = false; + + public WallpaperConnection(WallpaperInfo info, WallpaperData wallpaper) { + mInfo = info; + mWallpaper = wallpaper; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mLock) { + if (mWallpaper.connection == this) { + mWallpaper.lastDiedTime = SystemClock.uptimeMillis(); + mService = IWallpaperService.Stub.asInterface(service); + attachServiceLocked(this, mWallpaper); + // XXX should probably do saveSettingsLocked() later + // when we have an engine, but I'm not sure about + // locking there and anyway we always need to be able to + // recover if there is something wrong. + saveSettingsLocked(mWallpaper); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (mLock) { + mService = null; + mEngine = null; + if (mWallpaper.connection == this) { + Slog.w(TAG, "Wallpaper service gone: " + mWallpaper.wallpaperComponent); + if (!mWallpaper.wallpaperUpdating + && (mWallpaper.lastDiedTime + MIN_WALLPAPER_CRASH_TIME) + > SystemClock.uptimeMillis() + && mWallpaper.userId == mCurrentUserId) { + Slog.w(TAG, "Reverting to built-in wallpaper!"); + clearWallpaperLocked(true, mWallpaper.userId, null); + } + } + } + } + + @Override + public void attachEngine(IWallpaperEngine engine) { + synchronized (mLock) { + mEngine = engine; + if (mDimensionsChanged) { + try { + mEngine.setDesiredSize(mWallpaper.width, mWallpaper.height); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to set wallpaper dimensions", e); + } + mDimensionsChanged = false; + } + } + } + + @Override + public void engineShown(IWallpaperEngine engine) { + synchronized (mLock) { + if (mReply != null) { + long ident = Binder.clearCallingIdentity(); + try { + mReply.sendResult(null); + } catch (RemoteException e) { + Binder.restoreCallingIdentity(ident); + } + mReply = null; + } + } + } + + @Override + public ParcelFileDescriptor setWallpaper(String name) { + synchronized (mLock) { + if (mWallpaper.connection == this) { + return updateWallpaperBitmapLocked(name, mWallpaper); + } + return null; + } + } + } + + class MyPackageMonitor extends PackageMonitor { + @Override + public void onPackageUpdateFinished(String packageName, int uid) { + synchronized (mLock) { + if (mCurrentUserId != getChangingUserId()) { + return; + } + WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); + if (wallpaper != null) { + if (wallpaper.wallpaperComponent != null + && wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { + wallpaper.wallpaperUpdating = false; + ComponentName comp = wallpaper.wallpaperComponent; + clearWallpaperComponentLocked(wallpaper); + if (!bindWallpaperComponentLocked(comp, false, false, + wallpaper, null)) { + Slog.w(TAG, "Wallpaper no longer available; reverting to default"); + clearWallpaperLocked(false, wallpaper.userId, null); + } + } + } + } + } + + @Override + public void onPackageModified(String packageName) { + synchronized (mLock) { + if (mCurrentUserId != getChangingUserId()) { + return; + } + WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); + if (wallpaper != null) { + if (wallpaper.wallpaperComponent == null + || !wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { + return; + } + doPackagesChangedLocked(true, wallpaper); + } + } + } + + @Override + public void onPackageUpdateStarted(String packageName, int uid) { + synchronized (mLock) { + if (mCurrentUserId != getChangingUserId()) { + return; + } + WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); + if (wallpaper != null) { + if (wallpaper.wallpaperComponent != null + && wallpaper.wallpaperComponent.getPackageName().equals(packageName)) { + wallpaper.wallpaperUpdating = true; + } + } + } + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { + synchronized (mLock) { + boolean changed = false; + if (mCurrentUserId != getChangingUserId()) { + return false; + } + WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); + if (wallpaper != null) { + boolean res = doPackagesChangedLocked(doit, wallpaper); + changed |= res; + } + return changed; + } + } + + @Override + public void onSomePackagesChanged() { + synchronized (mLock) { + if (mCurrentUserId != getChangingUserId()) { + return; + } + WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); + if (wallpaper != null) { + doPackagesChangedLocked(true, wallpaper); + } + } + } + + boolean doPackagesChangedLocked(boolean doit, WallpaperData wallpaper) { + boolean changed = false; + if (wallpaper.wallpaperComponent != null) { + int change = isPackageDisappearing(wallpaper.wallpaperComponent + .getPackageName()); + if (change == PACKAGE_PERMANENT_CHANGE + || change == PACKAGE_TEMPORARY_CHANGE) { + changed = true; + if (doit) { + Slog.w(TAG, "Wallpaper uninstalled, removing: " + + wallpaper.wallpaperComponent); + clearWallpaperLocked(false, wallpaper.userId, null); + } + } + } + if (wallpaper.nextWallpaperComponent != null) { + int change = isPackageDisappearing(wallpaper.nextWallpaperComponent + .getPackageName()); + if (change == PACKAGE_PERMANENT_CHANGE + || change == PACKAGE_TEMPORARY_CHANGE) { + wallpaper.nextWallpaperComponent = null; + } + } + if (wallpaper.wallpaperComponent != null + && isPackageModified(wallpaper.wallpaperComponent.getPackageName())) { + try { + mContext.getPackageManager().getServiceInfo( + wallpaper.wallpaperComponent, 0); + } catch (NameNotFoundException e) { + Slog.w(TAG, "Wallpaper component gone, removing: " + + wallpaper.wallpaperComponent); + clearWallpaperLocked(false, wallpaper.userId, null); + } + } + if (wallpaper.nextWallpaperComponent != null + && isPackageModified(wallpaper.nextWallpaperComponent.getPackageName())) { + try { + mContext.getPackageManager().getServiceInfo( + wallpaper.nextWallpaperComponent, 0); + } catch (NameNotFoundException e) { + wallpaper.nextWallpaperComponent = null; + } + } + return changed; + } + } + + public WallpaperManagerService(Context context) { + if (DEBUG) Slog.v(TAG, "WallpaperService startup"); + mContext = context; + mIWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService(Context.WINDOW_SERVICE)); + mIPackageManager = AppGlobals.getPackageManager(); + mMonitor = new MyPackageMonitor(); + mMonitor.register(context, null, UserHandle.ALL, true); + getWallpaperDir(UserHandle.USER_OWNER).mkdirs(); + loadSettingsLocked(UserHandle.USER_OWNER); + } + + private static File getWallpaperDir(int userId) { + return Environment.getUserSystemDirectory(userId); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + for (int i = 0; i < mWallpaperMap.size(); i++) { + WallpaperData wallpaper = mWallpaperMap.valueAt(i); + wallpaper.wallpaperObserver.stopWatching(); + } + } + + public void systemRunning() { + if (DEBUG) Slog.v(TAG, "systemReady"); + WallpaperData wallpaper = mWallpaperMap.get(UserHandle.USER_OWNER); + switchWallpaper(wallpaper, null); + wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper); + wallpaper.wallpaperObserver.startWatching(); + + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + userFilter.addAction(Intent.ACTION_USER_STOPPING); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_REMOVED.equals(action)) { + onRemoveUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL)); + } + // TODO: Race condition causing problems when cleaning up on stopping a user. + // Comment this out for now. + // else if (Intent.ACTION_USER_STOPPING.equals(action)) { + // onStoppingUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + // UserHandle.USER_NULL)); + // } + } + }, userFilter); + + try { + ActivityManagerNative.getDefault().registerUserSwitchObserver( + new IUserSwitchObserver.Stub() { + @Override + public void onUserSwitching(int newUserId, IRemoteCallback reply) { + switchUser(newUserId, reply); + } + + @Override + public void onUserSwitchComplete(int newUserId) throws RemoteException { + } + }); + } catch (RemoteException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** Called by SystemBackupAgent */ + public String getName() { + // Verify caller is the system + if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) { + throw new RuntimeException("getName() can only be called from the system process"); + } + synchronized (mLock) { + return mWallpaperMap.get(0).name; + } + } + + void onStoppingUser(int userId) { + if (userId < 1) return; + synchronized (mLock) { + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper != null) { + if (wallpaper.wallpaperObserver != null) { + wallpaper.wallpaperObserver.stopWatching(); + wallpaper.wallpaperObserver = null; + } + mWallpaperMap.remove(userId); + } + } + } + + void onRemoveUser(int userId) { + if (userId < 1) return; + synchronized (mLock) { + onStoppingUser(userId); + File wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER); + wallpaperFile.delete(); + File wallpaperInfoFile = new File(getWallpaperDir(userId), WALLPAPER_INFO); + wallpaperInfoFile.delete(); + } + } + + void switchUser(int userId, IRemoteCallback reply) { + synchronized (mLock) { + mCurrentUserId = userId; + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper == null) { + wallpaper = new WallpaperData(userId); + mWallpaperMap.put(userId, wallpaper); + loadSettingsLocked(userId); + } + // Not started watching yet, in case wallpaper data was loaded for other reasons. + if (wallpaper.wallpaperObserver == null) { + wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper); + wallpaper.wallpaperObserver.startWatching(); + } + switchWallpaper(wallpaper, reply); + } + } + + void switchWallpaper(WallpaperData wallpaper, IRemoteCallback reply) { + synchronized (mLock) { + RuntimeException e = null; + try { + ComponentName cname = wallpaper.wallpaperComponent != null ? + wallpaper.wallpaperComponent : wallpaper.nextWallpaperComponent; + if (bindWallpaperComponentLocked(cname, true, false, wallpaper, reply)) { + return; + } + } catch (RuntimeException e1) { + e = e1; + } + Slog.w(TAG, "Failure starting previous wallpaper", e); + clearWallpaperLocked(false, wallpaper.userId, reply); + } + } + + public void clearWallpaper() { + if (DEBUG) Slog.v(TAG, "clearWallpaper"); + synchronized (mLock) { + clearWallpaperLocked(false, UserHandle.getCallingUserId(), null); + } + } + + void clearWallpaperLocked(boolean defaultFailed, int userId, IRemoteCallback reply) { + WallpaperData wallpaper = mWallpaperMap.get(userId); + File f = new File(getWallpaperDir(userId), WALLPAPER); + if (f.exists()) { + f.delete(); + } + final long ident = Binder.clearCallingIdentity(); + RuntimeException e = null; + try { + wallpaper.imageWallpaperPending = false; + if (userId != mCurrentUserId) return; + if (bindWallpaperComponentLocked(defaultFailed + ? IMAGE_WALLPAPER + : null, true, false, wallpaper, reply)) { + return; + } + } catch (IllegalArgumentException e1) { + e = e1; + } finally { + Binder.restoreCallingIdentity(ident); + } + + // This can happen if the default wallpaper component doesn't + // exist. This should be a system configuration problem, but + // let's not let it crash the system and just live with no + // wallpaper. + Slog.e(TAG, "Default wallpaper component not found!", e); + clearWallpaperComponentLocked(wallpaper); + if (reply != null) { + try { + reply.sendResult(null); + } catch (RemoteException e1) { + } + } + } + + public boolean hasNamedWallpaper(String name) { + synchronized (mLock) { + List users; + long ident = Binder.clearCallingIdentity(); + try { + users = ((UserManager) mContext.getSystemService(Context.USER_SERVICE)).getUsers(); + } finally { + Binder.restoreCallingIdentity(ident); + } + for (UserInfo user: users) { + WallpaperData wd = mWallpaperMap.get(user.id); + if (wd == null) { + // User hasn't started yet, so load her settings to peek at the wallpaper + loadSettingsLocked(user.id); + wd = mWallpaperMap.get(user.id); + } + if (wd != null && name.equals(wd.name)) { + return true; + } + } + } + return false; + } + + private Point getDefaultDisplaySize() { + Point p = new Point(); + WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); + Display d = wm.getDefaultDisplay(); + d.getRealSize(p); + return p; + } + + public void setDimensionHints(int width, int height) throws RemoteException { + checkPermission(android.Manifest.permission.SET_WALLPAPER_HINTS); + synchronized (mLock) { + int userId = UserHandle.getCallingUserId(); + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper == null) { + throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width and height must be > 0"); + } + // Make sure it is at least as large as the display. + Point displaySize = getDefaultDisplaySize(); + width = Math.max(width, displaySize.x); + height = Math.max(height, displaySize.y); + + if (width != wallpaper.width || height != wallpaper.height) { + wallpaper.width = width; + wallpaper.height = height; + saveSettingsLocked(wallpaper); + if (mCurrentUserId != userId) return; // Don't change the properties now + if (wallpaper.connection != null) { + if (wallpaper.connection.mEngine != null) { + try { + wallpaper.connection.mEngine.setDesiredSize( + width, height); + } catch (RemoteException e) { + } + notifyCallbacksLocked(wallpaper); + } else if (wallpaper.connection.mService != null) { + // We've attached to the service but the engine hasn't attached back to us + // yet. This means it will be created with the previous dimensions, so we + // need to update it to the new dimensions once it attaches. + wallpaper.connection.mDimensionsChanged = true; + } + } + } + } + } + + public int getWidthHint() throws RemoteException { + synchronized (mLock) { + WallpaperData wallpaper = mWallpaperMap.get(UserHandle.getCallingUserId()); + return wallpaper.width; + } + } + + public int getHeightHint() throws RemoteException { + synchronized (mLock) { + WallpaperData wallpaper = mWallpaperMap.get(UserHandle.getCallingUserId()); + return wallpaper.height; + } + } + + public ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb, + Bundle outParams) { + synchronized (mLock) { + // This returns the current user's wallpaper, if called by a system service. Else it + // returns the wallpaper for the calling user. + int callingUid = Binder.getCallingUid(); + int wallpaperUserId = 0; + if (callingUid == android.os.Process.SYSTEM_UID) { + wallpaperUserId = mCurrentUserId; + } else { + wallpaperUserId = UserHandle.getUserId(callingUid); + } + WallpaperData wallpaper = mWallpaperMap.get(wallpaperUserId); + try { + if (outParams != null) { + outParams.putInt("width", wallpaper.width); + outParams.putInt("height", wallpaper.height); + } + wallpaper.callbacks.register(cb); + File f = new File(getWallpaperDir(wallpaperUserId), WALLPAPER); + if (!f.exists()) { + return null; + } + return ParcelFileDescriptor.open(f, MODE_READ_ONLY); + } catch (FileNotFoundException e) { + /* Shouldn't happen as we check to see if the file exists */ + Slog.w(TAG, "Error getting wallpaper", e); + } + return null; + } + } + + public WallpaperInfo getWallpaperInfo() { + int userId = UserHandle.getCallingUserId(); + synchronized (mLock) { + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper.connection != null) { + return wallpaper.connection.mInfo; + } + return null; + } + } + + public ParcelFileDescriptor setWallpaper(String name) { + checkPermission(android.Manifest.permission.SET_WALLPAPER); + synchronized (mLock) { + if (DEBUG) Slog.v(TAG, "setWallpaper"); + int userId = UserHandle.getCallingUserId(); + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper == null) { + throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); + } + final long ident = Binder.clearCallingIdentity(); + try { + ParcelFileDescriptor pfd = updateWallpaperBitmapLocked(name, wallpaper); + if (pfd != null) { + wallpaper.imageWallpaperPending = true; + } + return pfd; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + ParcelFileDescriptor updateWallpaperBitmapLocked(String name, WallpaperData wallpaper) { + if (name == null) name = ""; + try { + File dir = getWallpaperDir(wallpaper.userId); + if (!dir.exists()) { + dir.mkdir(); + FileUtils.setPermissions( + dir.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + } + File file = new File(dir, WALLPAPER); + ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, + MODE_CREATE|MODE_READ_WRITE); + if (!SELinux.restorecon(file)) { + return null; + } + wallpaper.name = name; + return fd; + } catch (FileNotFoundException e) { + Slog.w(TAG, "Error setting wallpaper", e); + } + return null; + } + + public void setWallpaperComponent(ComponentName name) { + checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT); + synchronized (mLock) { + if (DEBUG) Slog.v(TAG, "setWallpaperComponent name=" + name); + int userId = UserHandle.getCallingUserId(); + WallpaperData wallpaper = mWallpaperMap.get(userId); + if (wallpaper == null) { + throw new IllegalStateException("Wallpaper not yet initialized for user " + userId); + } + final long ident = Binder.clearCallingIdentity(); + try { + wallpaper.imageWallpaperPending = false; + bindWallpaperComponentLocked(name, false, true, wallpaper, null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force, + boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) { + if (DEBUG) Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName); + // Has the component changed? + if (!force) { + if (wallpaper.connection != null) { + if (wallpaper.wallpaperComponent == null) { + if (componentName == null) { + if (DEBUG) Slog.v(TAG, "bindWallpaperComponentLocked: still using default"); + // Still using default wallpaper. + return true; + } + } else if (wallpaper.wallpaperComponent.equals(componentName)) { + // Changing to same wallpaper. + if (DEBUG) Slog.v(TAG, "same wallpaper"); + return true; + } + } + } + + try { + if (componentName == null) { + String defaultComponent = + mContext.getString(com.android.internal.R.string.default_wallpaper_component); + if (defaultComponent != null) { + // See if there is a default wallpaper component specified + componentName = ComponentName.unflattenFromString(defaultComponent); + if (DEBUG) Slog.v(TAG, "Use default component wallpaper:" + componentName); + } + if (componentName == null) { + // Fall back to static image wallpaper + componentName = IMAGE_WALLPAPER; + //clearWallpaperComponentLocked(); + //return; + if (DEBUG) Slog.v(TAG, "Using image wallpaper"); + } + } + int serviceUserId = wallpaper.userId; + ServiceInfo si = mIPackageManager.getServiceInfo(componentName, + PackageManager.GET_META_DATA | PackageManager.GET_PERMISSIONS, serviceUserId); + if (si == null) { + // The wallpaper component we're trying to use doesn't exist + Slog.w(TAG, "Attempted wallpaper " + componentName + " is unavailable"); + return false; + } + if (!android.Manifest.permission.BIND_WALLPAPER.equals(si.permission)) { + String msg = "Selected service does not require " + + android.Manifest.permission.BIND_WALLPAPER + + ": " + componentName; + if (fromUser) { + throw new SecurityException(msg); + } + Slog.w(TAG, msg); + return false; + } + + WallpaperInfo wi = null; + + Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); + if (componentName != null && !componentName.equals(IMAGE_WALLPAPER)) { + // Make sure the selected service is actually a wallpaper service. + List ris = + mIPackageManager.queryIntentServices(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, serviceUserId); + for (int i=0; i 4 && "res:".equals(wallpaper.name.substring(0, 4))) { + String resName = wallpaper.name.substring(4); + + String pkg = null; + int colon = resName.indexOf(':'); + if (colon > 0) { + pkg = resName.substring(0, colon); + } + + String ident = null; + int slash = resName.lastIndexOf('/'); + if (slash > 0) { + ident = resName.substring(slash+1); + } + + String type = null; + if (colon > 0 && slash > 0 && (slash-colon) > 1) { + type = resName.substring(colon+1, slash); + } + + if (pkg != null && ident != null && type != null) { + int resId = -1; + InputStream res = null; + FileOutputStream fos = null; + try { + Context c = mContext.createPackageContext(pkg, Context.CONTEXT_RESTRICTED); + Resources r = c.getResources(); + resId = r.getIdentifier(resName, null, null); + if (resId == 0) { + Slog.e(TAG, "couldn't resolve identifier pkg=" + pkg + " type=" + type + + " ident=" + ident); + return false; + } + + res = r.openRawResource(resId); + if (wallpaper.wallpaperFile.exists()) { + wallpaper.wallpaperFile.delete(); + } + fos = new FileOutputStream(wallpaper.wallpaperFile); + + byte[] buffer = new byte[32768]; + int amt; + while ((amt=res.read(buffer)) > 0) { + fos.write(buffer, 0, amt); + } + // mWallpaperObserver will notice the close and send the change broadcast + + Slog.v(TAG, "Restored wallpaper: " + resName); + return true; + } catch (NameNotFoundException e) { + Slog.e(TAG, "Package name " + pkg + " not found"); + } catch (Resources.NotFoundException e) { + Slog.e(TAG, "Resource not found: " + resId); + } catch (IOException e) { + Slog.e(TAG, "IOException while restoring wallpaper ", e); + } finally { + if (res != null) { + try { + res.close(); + } catch (IOException ex) {} + } + if (fos != null) { + FileUtils.sync(fos); + try { + fos.close(); + } catch (IOException ex) {} + } + } + } + } + return false; + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + pw.println("Permission Denial: can't dump wallpaper service from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mLock) { + pw.println("Current Wallpaper Service state:"); + for (int i = 0; i < mWallpaperMap.size(); i++) { + WallpaperData wallpaper = mWallpaperMap.valueAt(i); + pw.println(" User " + wallpaper.userId + ":"); + pw.print(" mWidth="); + pw.print(wallpaper.width); + pw.print(" mHeight="); + pw.println(wallpaper.height); + pw.print(" mName="); + pw.println(wallpaper.name); + pw.print(" mWallpaperComponent="); + pw.println(wallpaper.wallpaperComponent); + if (wallpaper.connection != null) { + WallpaperConnection conn = wallpaper.connection; + pw.print(" Wallpaper connection "); + pw.print(conn); + pw.println(":"); + if (conn.mInfo != null) { + pw.print(" mInfo.component="); + pw.println(conn.mInfo.getComponent()); + } + pw.print(" mToken="); + pw.println(conn.mToken); + pw.print(" mService="); + pw.println(conn.mService); + pw.print(" mEngine="); + pw.println(conn.mEngine); + pw.print(" mLastDiedTime="); + pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis()); + } + } + } + } +} -- cgit v1.2.3-59-g8ed1b