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