1/*
2 * Copyright (C) 2014 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.systemui.statusbar.policy;
18
19import android.app.ActivityManager;
20import android.app.ActivityManagerNative;
21import android.app.Dialog;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.pm.UserInfo;
28import android.database.ContentObserver;
29import android.graphics.Bitmap;
30import android.graphics.drawable.Drawable;
31import android.os.AsyncTask;
32import android.os.Handler;
33import android.os.RemoteException;
34import android.os.UserHandle;
35import android.os.UserManager;
36import android.provider.Settings;
37import android.util.Log;
38import android.util.SparseArray;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.BaseAdapter;
42
43import com.android.internal.util.UserIcons;
44import com.android.systemui.BitmapHelper;
45import com.android.systemui.GuestResumeSessionReceiver;
46import com.android.systemui.R;
47import com.android.systemui.qs.QSTile;
48import com.android.systemui.qs.tiles.UserDetailView;
49import com.android.systemui.statusbar.phone.SystemUIDialog;
50
51import java.io.FileDescriptor;
52import java.io.PrintWriter;
53import java.lang.ref.WeakReference;
54import java.util.ArrayList;
55import java.util.List;
56
57/**
58 * Keeps a list of all users on the device for user switching.
59 */
60public class UserSwitcherController {
61
62    private static final String TAG = "UserSwitcherController";
63    private static final boolean DEBUG = false;
64    private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING =
65            "lockscreenSimpleUserSwitcher";
66
67    private final Context mContext;
68    private final UserManager mUserManager;
69    private final ArrayList<WeakReference<BaseUserAdapter>> mAdapters = new ArrayList<>();
70    private final GuestResumeSessionReceiver mGuestResumeSessionReceiver
71            = new GuestResumeSessionReceiver();
72    private final KeyguardMonitor mKeyguardMonitor;
73
74    private ArrayList<UserRecord> mUsers = new ArrayList<>();
75    private Dialog mExitGuestDialog;
76    private Dialog mAddUserDialog;
77    private int mLastNonGuestUser = UserHandle.USER_OWNER;
78    private boolean mSimpleUserSwitcher;
79    private boolean mAddUsersWhenLocked;
80
81    public UserSwitcherController(Context context, KeyguardMonitor keyguardMonitor) {
82        mContext = context;
83        mGuestResumeSessionReceiver.register(context);
84        mKeyguardMonitor = keyguardMonitor;
85        mUserManager = UserManager.get(context);
86        IntentFilter filter = new IntentFilter();
87        filter.addAction(Intent.ACTION_USER_ADDED);
88        filter.addAction(Intent.ACTION_USER_REMOVED);
89        filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
90        filter.addAction(Intent.ACTION_USER_SWITCHED);
91        filter.addAction(Intent.ACTION_USER_STOPPING);
92        mContext.registerReceiverAsUser(mReceiver, UserHandle.OWNER, filter,
93                null /* permission */, null /* scheduler */);
94
95
96        mContext.getContentResolver().registerContentObserver(
97                Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true,
98                mSettingsObserver);
99        mContext.getContentResolver().registerContentObserver(
100                Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true,
101                mSettingsObserver);
102        // Fetch initial values.
103        mSettingsObserver.onChange(false);
104
105        keyguardMonitor.addCallback(mCallback);
106
107        refreshUsers(UserHandle.USER_NULL);
108    }
109
110    /**
111     * Refreshes users from UserManager.
112     *
113     * The pictures are only loaded if they have not been loaded yet.
114     *
115     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
116     */
117    @SuppressWarnings("unchecked")
118    private void refreshUsers(int forcePictureLoadForId) {
119
120        SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size());
121        final int N = mUsers.size();
122        for (int i = 0; i < N; i++) {
123            UserRecord r = mUsers.get(i);
124            if (r == null || r.info == null
125                    || r.info.id == forcePictureLoadForId || r.picture == null) {
126                continue;
127            }
128            bitmaps.put(r.info.id, r.picture);
129        }
130
131        final boolean addUsersWhenLocked = mAddUsersWhenLocked;
132        new AsyncTask<SparseArray<Bitmap>, Void, ArrayList<UserRecord>>() {
133            @SuppressWarnings("unchecked")
134            @Override
135            protected ArrayList<UserRecord> doInBackground(SparseArray<Bitmap>... params) {
136                final SparseArray<Bitmap> bitmaps = params[0];
137                List<UserInfo> infos = mUserManager.getUsers(true);
138                if (infos == null) {
139                    return null;
140                }
141                ArrayList<UserRecord> records = new ArrayList<>(infos.size());
142                int currentId = ActivityManager.getCurrentUser();
143                UserRecord guestRecord = null;
144                int avatarSize = mContext.getResources()
145                        .getDimensionPixelSize(R.dimen.max_avatar_size);
146
147                for (UserInfo info : infos) {
148                    boolean isCurrent = currentId == info.id;
149                    if (info.isGuest()) {
150                        guestRecord = new UserRecord(info, null /* picture */,
151                                true /* isGuest */, isCurrent, false /* isAddUser */,
152                                false /* isRestricted */);
153                    } else if (info.supportsSwitchTo()) {
154                        Bitmap picture = bitmaps.get(info.id);
155                        if (picture == null) {
156                            picture = mUserManager.getUserIcon(info.id);
157                        }
158                        if (picture != null) {
159                            picture = BitmapHelper.createCircularClip(
160                                    picture, avatarSize, avatarSize);
161                        }
162                        int index = isCurrent ? 0 : records.size();
163                        records.add(index, new UserRecord(info, picture, false /* isGuest */,
164                                isCurrent, false /* isAddUser */, false /* isRestricted */));
165                    }
166                }
167
168                boolean ownerCanCreateUsers = !mUserManager.hasUserRestriction(
169                        UserManager.DISALLOW_ADD_USER, UserHandle.OWNER);
170                boolean currentUserCanCreateUsers =
171                        (currentId == UserHandle.USER_OWNER) && ownerCanCreateUsers;
172                boolean anyoneCanCreateUsers = ownerCanCreateUsers && addUsersWhenLocked;
173                boolean canCreateGuest = (currentUserCanCreateUsers || anyoneCanCreateUsers)
174                        && guestRecord == null;
175                boolean canCreateUser = (currentUserCanCreateUsers || anyoneCanCreateUsers)
176                        && mUserManager.canAddMoreUsers();
177                boolean createIsRestricted = !addUsersWhenLocked;
178
179                if (!mSimpleUserSwitcher) {
180                    if (guestRecord == null) {
181                        if (canCreateGuest) {
182                            records.add(new UserRecord(null /* info */, null /* picture */,
183                                    true /* isGuest */, false /* isCurrent */,
184                                    false /* isAddUser */, createIsRestricted));
185                        }
186                    } else {
187                        int index = guestRecord.isCurrent ? 0 : records.size();
188                        records.add(index, guestRecord);
189                    }
190                }
191
192                if (!mSimpleUserSwitcher && canCreateUser) {
193                    records.add(new UserRecord(null /* info */, null /* picture */,
194                            false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
195                            createIsRestricted));
196                }
197
198                return records;
199            }
200
201            @Override
202            protected void onPostExecute(ArrayList<UserRecord> userRecords) {
203                if (userRecords != null) {
204                    mUsers = userRecords;
205                    notifyAdapters();
206                }
207            }
208        }.execute((SparseArray) bitmaps);
209    }
210
211    private void notifyAdapters() {
212        for (int i = mAdapters.size() - 1; i >= 0; i--) {
213            BaseUserAdapter adapter = mAdapters.get(i).get();
214            if (adapter != null) {
215                adapter.notifyDataSetChanged();
216            } else {
217                mAdapters.remove(i);
218            }
219        }
220    }
221
222    public boolean isSimpleUserSwitcher() {
223        return mSimpleUserSwitcher;
224    }
225
226    public void switchTo(UserRecord record) {
227        int id;
228        if (record.isGuest && record.info == null) {
229            // No guest user. Create one.
230            UserInfo guest = mUserManager.createGuest(
231                    mContext, mContext.getString(R.string.guest_nickname));
232            if (guest == null) {
233                // Couldn't create guest, most likely because there already exists one, we just
234                // haven't reloaded the user list yet.
235                return;
236            }
237            id = guest.id;
238        } else if (record.isAddUser) {
239            showAddUserDialog();
240            return;
241        } else {
242            id = record.info.id;
243        }
244
245        if (ActivityManager.getCurrentUser() == id) {
246            if (record.isGuest) {
247                showExitGuestDialog(id);
248            }
249            return;
250        }
251
252        switchToUserId(id);
253    }
254
255    private void switchToUserId(int id) {
256        try {
257            ActivityManagerNative.getDefault().switchUser(id);
258        } catch (RemoteException e) {
259            Log.e(TAG, "Couldn't switch user.", e);
260        }
261    }
262
263    private void showExitGuestDialog(int id) {
264        if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
265            mExitGuestDialog.cancel();
266        }
267        mExitGuestDialog = new ExitGuestDialog(mContext, id);
268        mExitGuestDialog.show();
269    }
270
271    private void showAddUserDialog() {
272        if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
273            mAddUserDialog.cancel();
274        }
275        mAddUserDialog = new AddUserDialog(mContext);
276        mAddUserDialog.show();
277    }
278
279    private void exitGuest(int id) {
280        int newId = UserHandle.USER_OWNER;
281        if (mLastNonGuestUser != UserHandle.USER_OWNER) {
282            UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
283            if (info != null && info.isEnabled() && info.supportsSwitchTo()) {
284                newId = info.id;
285            }
286        }
287        switchToUserId(newId);
288        mUserManager.removeUser(id);
289    }
290
291    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
292        @Override
293        public void onReceive(Context context, Intent intent) {
294            if (DEBUG) {
295                Log.v(TAG, "Broadcast: a=" + intent.getAction()
296                       + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
297            }
298            if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
299                if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
300                    mExitGuestDialog.cancel();
301                    mExitGuestDialog = null;
302                }
303
304                final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
305                final int N = mUsers.size();
306                for (int i = 0; i < N; i++) {
307                    UserRecord record = mUsers.get(i);
308                    if (record.info == null) continue;
309                    boolean shouldBeCurrent = record.info.id == currentId;
310                    if (record.isCurrent != shouldBeCurrent) {
311                        mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent));
312                    }
313                    if (shouldBeCurrent && !record.isGuest) {
314                        mLastNonGuestUser = record.info.id;
315                    }
316                    if (currentId != UserHandle.USER_OWNER && record.isRestricted) {
317                        // Immediately remove restricted records in case the AsyncTask is too slow.
318                        mUsers.remove(i);
319                        i--;
320                    }
321                }
322                notifyAdapters();
323            }
324            int forcePictureLoadForId = UserHandle.USER_NULL;
325            if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) {
326                forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
327                        UserHandle.USER_NULL);
328            }
329            refreshUsers(forcePictureLoadForId);
330        }
331    };
332
333    private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
334        public void onChange(boolean selfChange) {
335            mSimpleUserSwitcher = Settings.Global.getInt(mContext.getContentResolver(),
336                    SIMPLE_USER_SWITCHER_GLOBAL_SETTING, 0) != 0;
337            mAddUsersWhenLocked = Settings.Global.getInt(mContext.getContentResolver(),
338                    Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
339            refreshUsers(UserHandle.USER_NULL);
340        };
341    };
342
343    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
344        pw.println("UserSwitcherController state:");
345        pw.println("  mLastNonGuestUser=" + mLastNonGuestUser);
346        pw.print("  mUsers.size="); pw.println(mUsers.size());
347        for (int i = 0; i < mUsers.size(); i++) {
348            final UserRecord u = mUsers.get(i);
349            pw.print("    "); pw.println(u.toString());
350        }
351    }
352
353    public String getCurrentUserName(Context context) {
354        if (mUsers.isEmpty()) return null;
355        UserRecord item = mUsers.get(0);
356        if (item == null || item.info == null) return null;
357        if (item.isGuest) return context.getString(R.string.guest_nickname);
358        return item.info.name;
359    }
360
361    public static abstract class BaseUserAdapter extends BaseAdapter {
362
363        final UserSwitcherController mController;
364
365        protected BaseUserAdapter(UserSwitcherController controller) {
366            mController = controller;
367            controller.mAdapters.add(new WeakReference<>(this));
368        }
369
370        @Override
371        public int getCount() {
372            boolean secureKeyguardShowing = mController.mKeyguardMonitor.isShowing()
373                    && mController.mKeyguardMonitor.isSecure();
374            if (!secureKeyguardShowing) {
375                return mController.mUsers.size();
376            }
377            // The lock screen is secure and showing. Filter out restricted records.
378            final int N = mController.mUsers.size();
379            int count = 0;
380            for (int i = 0; i < N; i++) {
381                if (mController.mUsers.get(i).isRestricted) {
382                    break;
383                } else {
384                    count++;
385                }
386            }
387            return count;
388        }
389
390        @Override
391        public UserRecord getItem(int position) {
392            return mController.mUsers.get(position);
393        }
394
395        @Override
396        public long getItemId(int position) {
397            return position;
398        }
399
400        public void switchTo(UserRecord record) {
401            mController.switchTo(record);
402        }
403
404        public String getName(Context context, UserRecord item) {
405            if (item.isGuest) {
406                if (item.isCurrent) {
407                    return context.getString(R.string.guest_exit_guest);
408                } else {
409                    return context.getString(
410                            item.info == null ? R.string.guest_new_guest : R.string.guest_nickname);
411                }
412            } else if (item.isAddUser) {
413                return context.getString(R.string.user_add_user);
414            } else {
415                return item.info.name;
416            }
417        }
418
419        public int getSwitchableUsers() {
420            int result = 0;
421            ArrayList<UserRecord> users = mController.mUsers;
422            int N = users.size();
423            for (int i = 0; i < N; i++) {
424                if (users.get(i).info != null) {
425                    result++;
426                }
427            }
428            return result;
429        }
430
431        public Drawable getDrawable(Context context, UserRecord item) {
432            if (item.isAddUser) {
433                return context.getDrawable(R.drawable.ic_add_circle_qs);
434            }
435            return UserIcons.getDefaultUserIcon(item.isGuest ? UserHandle.USER_NULL : item.info.id,
436                    /* light= */ true);
437        }
438    }
439
440    public static final class UserRecord {
441        public final UserInfo info;
442        public final Bitmap picture;
443        public final boolean isGuest;
444        public final boolean isCurrent;
445        public final boolean isAddUser;
446        /** If true, the record is only visible to the owner and only when unlocked. */
447        public final boolean isRestricted;
448
449        public UserRecord(UserInfo info, Bitmap picture, boolean isGuest, boolean isCurrent,
450                boolean isAddUser, boolean isRestricted) {
451            this.info = info;
452            this.picture = picture;
453            this.isGuest = isGuest;
454            this.isCurrent = isCurrent;
455            this.isAddUser = isAddUser;
456            this.isRestricted = isRestricted;
457        }
458
459        public UserRecord copyWithIsCurrent(boolean _isCurrent) {
460            return new UserRecord(info, picture, isGuest, _isCurrent, isAddUser, isRestricted);
461        }
462
463        public String toString() {
464            StringBuilder sb = new StringBuilder();
465            sb.append("UserRecord(");
466            if (info != null) {
467                sb.append("name=\"" + info.name + "\" id=" + info.id);
468            } else {
469                if (isGuest) {
470                    sb.append("<add guest placeholder>");
471                } else if (isAddUser) {
472                    sb.append("<add user placeholder>");
473                }
474            }
475            if (isGuest) sb.append(" <isGuest>");
476            if (isAddUser) sb.append(" <isAddUser>");
477            if (isCurrent) sb.append(" <isCurrent>");
478            if (picture != null) sb.append(" <hasPicture>");
479            if (isRestricted) sb.append(" <isRestricted>");
480            sb.append(')');
481            return sb.toString();
482        }
483    }
484
485    public final QSTile.DetailAdapter userDetailAdapter = new QSTile.DetailAdapter() {
486        private final Intent USER_SETTINGS_INTENT = new Intent("android.settings.USER_SETTINGS");
487
488        @Override
489        public int getTitle() {
490            return R.string.quick_settings_user_title;
491        }
492
493        @Override
494        public View createDetailView(Context context, View convertView, ViewGroup parent) {
495            UserDetailView v;
496            if (!(convertView instanceof UserDetailView)) {
497                v = UserDetailView.inflate(context, parent, false);
498                v.createAndSetAdapter(UserSwitcherController.this);
499            } else {
500                v = (UserDetailView) convertView;
501            }
502            return v;
503        }
504
505        @Override
506        public Intent getSettingsIntent() {
507            return USER_SETTINGS_INTENT;
508        }
509
510        @Override
511        public Boolean getToggleState() {
512            return null;
513        }
514
515        @Override
516        public void setToggleState(boolean state) {
517        }
518    };
519
520    private final KeyguardMonitor.Callback mCallback = new KeyguardMonitor.Callback() {
521        @Override
522        public void onKeyguardChanged() {
523            notifyAdapters();
524        }
525    };
526
527    private final class ExitGuestDialog extends SystemUIDialog implements
528            DialogInterface.OnClickListener {
529
530        private final int mGuestId;
531
532        public ExitGuestDialog(Context context, int guestId) {
533            super(context);
534            setTitle(R.string.guest_exit_guest_dialog_title);
535            setMessage(context.getString(R.string.guest_exit_guest_dialog_message));
536            setButton(DialogInterface.BUTTON_NEGATIVE,
537                    context.getString(android.R.string.cancel), this);
538            setButton(DialogInterface.BUTTON_POSITIVE,
539                    context.getString(R.string.guest_exit_guest_dialog_remove), this);
540            setCanceledOnTouchOutside(false);
541            mGuestId = guestId;
542        }
543
544        @Override
545        public void onClick(DialogInterface dialog, int which) {
546            if (which == BUTTON_NEGATIVE) {
547                cancel();
548            } else {
549                dismiss();
550                exitGuest(mGuestId);
551            }
552        }
553    }
554
555    private final class AddUserDialog extends SystemUIDialog implements
556            DialogInterface.OnClickListener {
557
558        public AddUserDialog(Context context) {
559            super(context);
560            setTitle(R.string.user_add_user_title);
561            setMessage(context.getString(R.string.user_add_user_message_short));
562            setButton(DialogInterface.BUTTON_NEGATIVE,
563                    context.getString(android.R.string.cancel), this);
564            setButton(DialogInterface.BUTTON_POSITIVE,
565                    context.getString(android.R.string.ok), this);
566        }
567
568        @Override
569        public void onClick(DialogInterface dialog, int which) {
570            if (which == BUTTON_NEGATIVE) {
571                cancel();
572            } else {
573                dismiss();
574                if (ActivityManager.isUserAMonkey()) {
575                    return;
576                }
577                UserInfo user = mUserManager.createSecondaryUser(
578                        mContext.getString(R.string.user_new_user_name), 0 /* flags */);
579                if (user == null) {
580                    // Couldn't create user, most likely because there are too many, but we haven't
581                    // been able to reload the list yet.
582                    return;
583                }
584                int id = user.id;
585                Bitmap icon = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(
586                        id, /* light= */ false));
587                mUserManager.setUserIcon(id, icon);
588                switchToUserId(id);
589            }
590        }
591    }
592}
593