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
17package com.android.backupconfirm;
18
19import android.app.Activity;
20import android.app.backup.FullBackup;
21import android.app.backup.IBackupManager;
22import android.app.backup.IFullBackupRestoreObserver;
23import android.content.Context;
24import android.content.Intent;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.Message;
28import android.os.RemoteException;
29import android.os.ServiceManager;
30import android.os.storage.IMountService;
31import android.os.storage.StorageManager;
32import android.text.Editable;
33import android.text.TextWatcher;
34import android.util.Slog;
35import android.view.View;
36import android.widget.Button;
37import android.widget.TextView;
38import 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 */
51public class BackupRestoreConfirmation extends Activity {
52    static final String TAG = "BackupRestoreConfirmation";
53    static final boolean DEBUG = true;
54
55    static final String DID_ACKNOWLEDGE = "did_acknowledge";
56
57    static final int MSG_START_BACKUP = 1;
58    static final int MSG_BACKUP_PACKAGE = 2;
59    static final int MSG_END_BACKUP = 3;
60    static final int MSG_START_RESTORE = 11;
61    static final int MSG_RESTORE_PACKAGE = 12;
62    static final int MSG_END_RESTORE = 13;
63    static final int MSG_TIMEOUT = 100;
64
65    Handler mHandler;
66    IBackupManager mBackupManager;
67    IMountService mMountService;
68    FullObserver mObserver;
69    int mToken;
70    boolean mIsEncrypted;
71    boolean mDidAcknowledge;
72
73    TextView mStatusView;
74    TextView mCurPassword;
75    TextView mEncPassword;
76    Button mAllowButton;
77    Button mDenyButton;
78
79    // Handler for dealing with observer callbacks on the main thread
80    class ObserverHandler extends Handler {
81        Context mContext;
82        ObserverHandler(Context context) {
83            mContext = context;
84            mDidAcknowledge = false;
85        }
86
87        @Override
88        public void handleMessage(Message msg) {
89            switch (msg.what) {
90                case MSG_START_BACKUP: {
91                    Toast.makeText(mContext, R.string.toast_backup_started, Toast.LENGTH_LONG).show();
92                }
93                break;
94
95                case MSG_BACKUP_PACKAGE: {
96                    String name = (String) msg.obj;
97                    mStatusView.setText(name);
98                }
99                break;
100
101                case MSG_END_BACKUP: {
102                    Toast.makeText(mContext, R.string.toast_backup_ended, Toast.LENGTH_LONG).show();
103                    finish();
104                }
105                break;
106
107                case MSG_START_RESTORE: {
108                    Toast.makeText(mContext, R.string.toast_restore_started, Toast.LENGTH_LONG).show();
109                }
110                break;
111
112                case MSG_RESTORE_PACKAGE: {
113                    String name = (String) msg.obj;
114                    mStatusView.setText(name);
115                }
116                break;
117
118                case MSG_END_RESTORE: {
119                    Toast.makeText(mContext, R.string.toast_restore_ended, Toast.LENGTH_SHORT).show();
120                    finish();
121                }
122                break;
123
124                case MSG_TIMEOUT: {
125                    Toast.makeText(mContext, R.string.toast_timeout, Toast.LENGTH_LONG).show();
126                }
127                break;
128            }
129        }
130    }
131
132    @Override
133    public void onCreate(Bundle icicle) {
134        super.onCreate(icicle);
135
136        final Intent intent = getIntent();
137        final String action = intent.getAction();
138
139        final int layoutId;
140        final int titleId;
141        if (action.equals(FullBackup.FULL_BACKUP_INTENT_ACTION)) {
142            layoutId = R.layout.confirm_backup;
143            titleId = R.string.backup_confirm_title;
144        } else if (action.equals(FullBackup.FULL_RESTORE_INTENT_ACTION)) {
145            layoutId = R.layout.confirm_restore;
146            titleId = R.string.restore_confirm_title;
147        } else {
148            Slog.w(TAG, "Backup/restore confirmation activity launched with invalid action!");
149            finish();
150            return;
151        }
152
153        mToken = intent.getIntExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, -1);
154        if (mToken < 0) {
155            Slog.e(TAG, "Backup/restore confirmation requested but no token passed!");
156            finish();
157            return;
158        }
159
160        mBackupManager = IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE));
161        mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount"));
162
163        mHandler = new ObserverHandler(getApplicationContext());
164        final Object oldObserver = getLastNonConfigurationInstance();
165        if (oldObserver == null) {
166            mObserver = new FullObserver(mHandler);
167        } else {
168            mObserver = (FullObserver) oldObserver;
169            mObserver.setHandler(mHandler);
170        }
171
172        setTitle(titleId);
173        setContentView(layoutId);
174
175        // Same resource IDs for each layout variant (backup / restore)
176        mStatusView = (TextView) findViewById(R.id.package_name);
177        mAllowButton = (Button) findViewById(R.id.button_allow);
178        mDenyButton = (Button) findViewById(R.id.button_deny);
179
180        mCurPassword = (TextView) findViewById(R.id.password);
181        mEncPassword = (TextView) findViewById(R.id.enc_password);
182        TextView curPwDesc = (TextView) findViewById(R.id.password_desc);
183
184        mAllowButton.setOnClickListener(new View.OnClickListener() {
185            @Override
186            public void onClick(View v) {
187                sendAcknowledgement(mToken, true, mObserver);
188                mAllowButton.setEnabled(false);
189                mDenyButton.setEnabled(false);
190            }
191        });
192
193        mDenyButton.setOnClickListener(new View.OnClickListener() {
194            @Override
195            public void onClick(View v) {
196                sendAcknowledgement(mToken, false, mObserver);
197                mAllowButton.setEnabled(false);
198                mDenyButton.setEnabled(false);
199                finish();
200            }
201        });
202
203        // if we're a relaunch we may need to adjust button enable state
204        if (icicle != null) {
205            mDidAcknowledge = icicle.getBoolean(DID_ACKNOWLEDGE, false);
206            mAllowButton.setEnabled(!mDidAcknowledge);
207            mDenyButton.setEnabled(!mDidAcknowledge);
208        }
209
210        // We vary the password prompt depending on whether one is predefined, and whether
211        // the device is encrypted.
212        mIsEncrypted = deviceIsEncrypted();
213        if (!haveBackupPassword()) {
214            curPwDesc.setVisibility(View.GONE);
215            mCurPassword.setVisibility(View.GONE);
216            if (layoutId == R.layout.confirm_backup) {
217                TextView encPwDesc = (TextView) findViewById(R.id.enc_password_desc);
218                if (mIsEncrypted) {
219                    encPwDesc.setText(R.string.backup_enc_password_required);
220                    monitorEncryptionPassword();
221                } else {
222                    encPwDesc.setText(R.string.backup_enc_password_optional);
223                }
224            }
225        }
226    }
227
228    private void monitorEncryptionPassword() {
229        mAllowButton.setEnabled(false);
230        mEncPassword.addTextChangedListener(new TextWatcher() {
231            @Override
232            public void onTextChanged(CharSequence s, int start, int before, int count) { }
233
234            @Override
235            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
236
237            @Override
238            public void afterTextChanged(Editable s) {
239                mAllowButton.setEnabled(mEncPassword.getText().length() > 0);
240            }
241        });
242    }
243
244    // Preserve the restore observer callback binder across activity relaunch
245    @Override
246    public Object onRetainNonConfigurationInstance() {
247        return mObserver;
248    }
249
250    @Override
251    protected void onSaveInstanceState(Bundle outState) {
252        outState.putBoolean(DID_ACKNOWLEDGE, mDidAcknowledge);
253    }
254
255    void sendAcknowledgement(int token, boolean allow, IFullBackupRestoreObserver observer) {
256        if (!mDidAcknowledge) {
257            mDidAcknowledge = true;
258
259            try {
260                CharSequence encPassword = mEncPassword.getText();
261                mBackupManager.acknowledgeFullBackupOrRestore(mToken,
262                        allow,
263                        String.valueOf(mCurPassword.getText()),
264                        String.valueOf(encPassword),
265                        mObserver);
266            } catch (RemoteException e) {
267                // TODO: bail gracefully if we can't contact the backup manager
268            }
269        }
270    }
271
272    boolean deviceIsEncrypted() {
273        try {
274            return mMountService.getEncryptionState()
275                     != IMountService.ENCRYPTION_STATE_NONE
276                && mMountService.getPasswordType()
277                     != StorageManager.CRYPT_TYPE_DEFAULT;
278        } catch (Exception e) {
279            // If we can't talk to the mount service we have a serious problem; fail
280            // "secure" i.e. assuming that the device is encrypted.
281            Slog.e(TAG, "Unable to communicate with mount service: " + e.getMessage());
282            return true;
283        }
284    }
285
286    boolean haveBackupPassword() {
287        try {
288            return mBackupManager.hasBackupPassword();
289        } catch (RemoteException e) {
290            return true;        // in the failure case, assume we need one
291        }
292    }
293
294    /**
295     * The observer binder for showing backup/restore progress.  This binder just bounces
296     * the notifications onto the main thread.
297     */
298    class FullObserver extends IFullBackupRestoreObserver.Stub {
299        private Handler mHandler;
300
301        public FullObserver(Handler h) {
302            mHandler = h;
303        }
304
305        public void setHandler(Handler h) {
306            mHandler = h;
307        }
308
309        //
310        // IFullBackupRestoreObserver implementation
311        //
312        @Override
313        public void onStartBackup() throws RemoteException {
314            mHandler.sendEmptyMessage(MSG_START_BACKUP);
315        }
316
317        @Override
318        public void onBackupPackage(String name) throws RemoteException {
319            mHandler.sendMessage(mHandler.obtainMessage(MSG_BACKUP_PACKAGE, name));
320        }
321
322        @Override
323        public void onEndBackup() throws RemoteException {
324            mHandler.sendEmptyMessage(MSG_END_BACKUP);
325        }
326
327        @Override
328        public void onStartRestore() throws RemoteException {
329            mHandler.sendEmptyMessage(MSG_START_RESTORE);
330        }
331
332        @Override
333        public void onRestorePackage(String name) throws RemoteException {
334            mHandler.sendMessage(mHandler.obtainMessage(MSG_RESTORE_PACKAGE, name));
335        }
336
337        @Override
338        public void onEndRestore() throws RemoteException {
339            mHandler.sendEmptyMessage(MSG_END_RESTORE);
340        }
341
342        @Override
343        public void onTimeout() throws RemoteException {
344            mHandler.sendEmptyMessage(MSG_TIMEOUT);
345        }
346    }
347}
348