1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.backupconfirm; 18 19 import android.app.Activity; 20 import android.app.backup.FullBackup; 21 import android.app.backup.IBackupManager; 22 import android.app.backup.IFullBackupRestoreObserver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Message; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.os.storage.IStorageManager; 31 import android.os.storage.StorageManager; 32 import android.text.Editable; 33 import android.text.TextWatcher; 34 import android.util.Slog; 35 import android.view.View; 36 import android.widget.Button; 37 import android.widget.TextView; 38 import android.widget.Toast; 39 40 /** 41 * Confirm with the user that a requested full backup/restore operation is legitimate. 42 * Any attempt to perform a full backup/restore will launch this UI and wait for a 43 * designated timeout interval (nominally 30 seconds) for the user to confirm. If the 44 * user fails to respond within the timeout period, or explicitly refuses the operation 45 * within the UI presented here, no data will be transferred off the device. 46 * 47 * Note that the fully scoped name of this class is baked into the backup manager service. 48 * 49 * @hide 50 */ 51 public class BackupRestoreConfirmation extends Activity { 52 static final String TAG = "BackupRestoreConfirmation"; 53 static final boolean DEBUG = true; 54 55 static final String KEY_DID_ACKNOWLEDGE = "did_acknowledge"; 56 static final String KEY_TOKEN = "token"; 57 static final String KEY_ACTION = "action"; 58 59 static final int MSG_START_BACKUP = 1; 60 static final int MSG_BACKUP_PACKAGE = 2; 61 static final int MSG_END_BACKUP = 3; 62 static final int MSG_START_RESTORE = 11; 63 static final int MSG_RESTORE_PACKAGE = 12; 64 static final int MSG_END_RESTORE = 13; 65 static final int MSG_TIMEOUT = 100; 66 67 Handler mHandler; 68 IBackupManager mBackupManager; 69 IStorageManager mStorageManager; 70 FullObserver mObserver; 71 int mToken; 72 boolean mIsEncrypted; 73 boolean mDidAcknowledge; 74 String mAction; 75 76 TextView mStatusView; 77 TextView mCurPassword; 78 TextView mEncPassword; 79 Button mAllowButton; 80 Button mDenyButton; 81 82 // Handler for dealing with observer callbacks on the main thread 83 class ObserverHandler extends Handler { 84 Context mContext; ObserverHandler(Context context)85 ObserverHandler(Context context) { 86 mContext = context; 87 mDidAcknowledge = false; 88 } 89 90 @Override handleMessage(Message msg)91 public void handleMessage(Message msg) { 92 switch (msg.what) { 93 case MSG_START_BACKUP: { 94 Toast.makeText(mContext, R.string.toast_backup_started, Toast.LENGTH_LONG).show(); 95 } 96 break; 97 98 case MSG_BACKUP_PACKAGE: { 99 String name = (String) msg.obj; 100 mStatusView.setText(name); 101 } 102 break; 103 104 case MSG_END_BACKUP: { 105 Toast.makeText(mContext, R.string.toast_backup_ended, Toast.LENGTH_LONG).show(); 106 finish(); 107 } 108 break; 109 110 case MSG_START_RESTORE: { 111 Toast.makeText(mContext, R.string.toast_restore_started, Toast.LENGTH_LONG).show(); 112 } 113 break; 114 115 case MSG_RESTORE_PACKAGE: { 116 String name = (String) msg.obj; 117 mStatusView.setText(name); 118 } 119 break; 120 121 case MSG_END_RESTORE: { 122 Toast.makeText(mContext, R.string.toast_restore_ended, Toast.LENGTH_SHORT).show(); 123 finish(); 124 } 125 break; 126 127 case MSG_TIMEOUT: { 128 Toast.makeText(mContext, R.string.toast_timeout, Toast.LENGTH_LONG).show(); 129 } 130 break; 131 } 132 } 133 } 134 135 @Override onCreate(Bundle icicle)136 public void onCreate(Bundle icicle) { 137 super.onCreate(icicle); 138 139 final Intent intent = getIntent(); 140 141 boolean tokenValid = setTokenOrFinish(intent, icicle); 142 if (!tokenValid) { // already called finish() 143 return; 144 } 145 146 mBackupManager = IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE)); 147 mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); 148 149 mHandler = new ObserverHandler(getApplicationContext()); 150 final Object oldObserver = getLastNonConfigurationInstance(); 151 if (oldObserver == null) { 152 mObserver = new FullObserver(mHandler); 153 } else { 154 mObserver = (FullObserver) oldObserver; 155 mObserver.setHandler(mHandler); 156 } 157 158 setViews(intent, icicle); 159 } 160 161 @Override onNewIntent(Intent intent)162 public void onNewIntent(Intent intent) { 163 super.onNewIntent(intent); 164 setIntent(intent); 165 166 boolean tokenValid = setTokenOrFinish(intent, null); 167 if (!tokenValid) { // already called finish() 168 return; 169 } 170 171 setViews(intent, null); 172 } 173 setTokenOrFinish(Intent intent, Bundle icicle)174 private boolean setTokenOrFinish(Intent intent, Bundle icicle) { 175 mToken = intent.getIntExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, -1); 176 177 // for relaunch, we try to use the last token before exit 178 if (icicle != null) { 179 mToken = icicle.getInt(KEY_TOKEN, mToken); 180 } 181 182 if (mToken < 0) { 183 Slog.e(TAG, "Backup/restore confirmation requested but no token passed!"); 184 finish(); 185 return false; 186 } 187 188 return true; 189 } 190 setViews(Intent intent, Bundle icicle)191 private void setViews(Intent intent, Bundle icicle) { 192 mAction = intent.getAction(); 193 194 // for relaunch, we try to use the last action before exit 195 if (icicle != null) { 196 mAction = icicle.getString(KEY_ACTION, mAction); 197 } 198 199 final int layoutId; 200 final int titleId; 201 if (mAction.equals(FullBackup.FULL_BACKUP_INTENT_ACTION)) { 202 layoutId = R.layout.confirm_backup; 203 titleId = R.string.backup_confirm_title; 204 } else if (mAction.equals(FullBackup.FULL_RESTORE_INTENT_ACTION)) { 205 layoutId = R.layout.confirm_restore; 206 titleId = R.string.restore_confirm_title; 207 } else { 208 Slog.w(TAG, "Backup/restore confirmation activity launched with invalid action!"); 209 finish(); 210 return; 211 } 212 213 setTitle(titleId); 214 setContentView(layoutId); 215 216 // Same resource IDs for each layout variant (backup / restore) 217 mStatusView = findViewById(R.id.package_name); 218 mAllowButton = findViewById(R.id.button_allow); 219 mDenyButton = findViewById(R.id.button_deny); 220 221 mCurPassword = findViewById(R.id.password); 222 mEncPassword = findViewById(R.id.enc_password); 223 TextView curPwDesc = findViewById(R.id.password_desc); 224 225 mAllowButton.setOnClickListener(new View.OnClickListener() { 226 @Override 227 public void onClick(View v) { 228 sendAcknowledgement(mToken, true, mObserver); 229 mAllowButton.setEnabled(false); 230 mDenyButton.setEnabled(false); 231 } 232 }); 233 234 mDenyButton.setOnClickListener(new View.OnClickListener() { 235 @Override 236 public void onClick(View v) { 237 sendAcknowledgement(mToken, false, mObserver); 238 mAllowButton.setEnabled(false); 239 mDenyButton.setEnabled(false); 240 finish(); 241 } 242 }); 243 244 // if we're a relaunch we may need to adjust button enable state 245 if (icicle != null) { 246 mDidAcknowledge = icicle.getBoolean(KEY_DID_ACKNOWLEDGE, false); 247 mAllowButton.setEnabled(!mDidAcknowledge); 248 mDenyButton.setEnabled(!mDidAcknowledge); 249 } 250 251 // We vary the password prompt depending on whether one is predefined, and whether 252 // the device is encrypted. 253 mIsEncrypted = deviceIsEncrypted(); 254 if (!haveBackupPassword()) { 255 curPwDesc.setVisibility(View.GONE); 256 mCurPassword.setVisibility(View.GONE); 257 if (layoutId == R.layout.confirm_backup) { 258 TextView encPwDesc = findViewById(R.id.enc_password_desc); 259 if (mIsEncrypted) { 260 encPwDesc.setText(R.string.backup_enc_password_required); 261 monitorEncryptionPassword(); 262 } else { 263 encPwDesc.setText(R.string.backup_enc_password_optional); 264 } 265 } 266 } 267 } 268 monitorEncryptionPassword()269 private void monitorEncryptionPassword() { 270 mAllowButton.setEnabled(false); 271 mEncPassword.addTextChangedListener(new TextWatcher() { 272 @Override 273 public void onTextChanged(CharSequence s, int start, int before, int count) { } 274 275 @Override 276 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 277 278 @Override 279 public void afterTextChanged(Editable s) { 280 mAllowButton.setEnabled(mEncPassword.getText().length() > 0); 281 } 282 }); 283 } 284 285 // Preserve the restore observer callback binder across activity relaunch 286 @Override onRetainNonConfigurationInstance()287 public Object onRetainNonConfigurationInstance() { 288 return mObserver; 289 } 290 291 @Override onSaveInstanceState(Bundle outState)292 protected void onSaveInstanceState(Bundle outState) { 293 outState.putBoolean(KEY_DID_ACKNOWLEDGE, mDidAcknowledge); 294 outState.putInt(KEY_TOKEN, mToken); 295 outState.putString(KEY_ACTION, mAction); 296 } 297 sendAcknowledgement(int token, boolean allow, IFullBackupRestoreObserver observer)298 void sendAcknowledgement(int token, boolean allow, IFullBackupRestoreObserver observer) { 299 if (!mDidAcknowledge) { 300 mDidAcknowledge = true; 301 302 try { 303 CharSequence encPassword = mEncPassword.getText(); 304 mBackupManager.acknowledgeFullBackupOrRestore(mToken, 305 allow, 306 String.valueOf(mCurPassword.getText()), 307 String.valueOf(encPassword), 308 mObserver); 309 } catch (RemoteException e) { 310 // TODO: bail gracefully if we can't contact the backup manager 311 } 312 } 313 } 314 deviceIsEncrypted()315 boolean deviceIsEncrypted() { 316 try { 317 return mStorageManager.getEncryptionState() 318 != StorageManager.ENCRYPTION_STATE_NONE 319 && mStorageManager.getPasswordType() 320 != StorageManager.CRYPT_TYPE_DEFAULT; 321 } catch (Exception e) { 322 // If we can't talk to the storagemanager service we have a serious problem; fail 323 // "secure" i.e. assuming that the device is encrypted. 324 Slog.e(TAG, "Unable to communicate with storagemanager service: " + e.getMessage()); 325 return true; 326 } 327 } 328 haveBackupPassword()329 boolean haveBackupPassword() { 330 try { 331 return mBackupManager.hasBackupPassword(); 332 } catch (RemoteException e) { 333 return true; // in the failure case, assume we need one 334 } 335 } 336 337 /** 338 * The observer binder for showing backup/restore progress. This binder just bounces 339 * the notifications onto the main thread. 340 */ 341 class FullObserver extends IFullBackupRestoreObserver.Stub { 342 private Handler mHandler; 343 FullObserver(Handler h)344 public FullObserver(Handler h) { 345 mHandler = h; 346 } 347 setHandler(Handler h)348 public void setHandler(Handler h) { 349 mHandler = h; 350 } 351 352 // 353 // IFullBackupRestoreObserver implementation 354 // 355 @Override onStartBackup()356 public void onStartBackup() throws RemoteException { 357 mHandler.sendEmptyMessage(MSG_START_BACKUP); 358 } 359 360 @Override onBackupPackage(String name)361 public void onBackupPackage(String name) throws RemoteException { 362 mHandler.sendMessage(mHandler.obtainMessage(MSG_BACKUP_PACKAGE, name)); 363 } 364 365 @Override onEndBackup()366 public void onEndBackup() throws RemoteException { 367 mHandler.sendEmptyMessage(MSG_END_BACKUP); 368 } 369 370 @Override onStartRestore()371 public void onStartRestore() throws RemoteException { 372 mHandler.sendEmptyMessage(MSG_START_RESTORE); 373 } 374 375 @Override onRestorePackage(String name)376 public void onRestorePackage(String name) throws RemoteException { 377 mHandler.sendMessage(mHandler.obtainMessage(MSG_RESTORE_PACKAGE, name)); 378 } 379 380 @Override onEndRestore()381 public void onEndRestore() throws RemoteException { 382 mHandler.sendEmptyMessage(MSG_END_RESTORE); 383 } 384 385 @Override onTimeout()386 public void onTimeout() throws RemoteException { 387 mHandler.sendEmptyMessage(MSG_TIMEOUT); 388 } 389 } 390 } 391