| /* |
| * Copyright (C) 2016 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.retaildemo; |
| |
| import android.Manifest; |
| import android.app.ActivityManagerInternal; |
| import android.app.ActivityManagerNative; |
| import android.app.AppGlobals; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.app.RetailDemoModeServiceInternal; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.UserInfo; |
| import android.content.res.Configuration; |
| import android.database.ContentObserver; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraCharacteristics; |
| import android.hardware.camera2.CameraManager; |
| import android.media.AudioManager; |
| import android.media.AudioSystem; |
| import android.net.Uri; |
| import android.os.Environment; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.provider.CallLog; |
| import android.provider.MediaStore; |
| import android.provider.Settings; |
| import android.util.KeyValueListParser; |
| import android.util.Slog; |
| import com.android.internal.os.BackgroundThread; |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.server.LocalServices; |
| import com.android.server.ServiceThread; |
| import com.android.server.SystemService; |
| import com.android.server.am.ActivityManagerService; |
| import com.android.server.retaildemo.UserInactivityCountdownDialog.OnCountDownExpiredListener; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| |
| public class RetailDemoModeService extends SystemService { |
| private static final boolean DEBUG = false; |
| |
| private static final String TAG = RetailDemoModeService.class.getSimpleName(); |
| private static final String DEMO_USER_NAME = "Demo"; |
| private static final String ACTION_RESET_DEMO = |
| "com.android.server.retaildemo.ACTION_RESET_DEMO"; |
| |
| private static final int MSG_TURN_SCREEN_ON = 0; |
| private static final int MSG_INACTIVITY_TIME_OUT = 1; |
| private static final int MSG_START_NEW_SESSION = 2; |
| |
| private static final long SCREEN_WAKEUP_DELAY = 2500; |
| private static final long USER_INACTIVITY_TIMEOUT_MIN = 10000; |
| private static final long USER_INACTIVITY_TIMEOUT_DEFAULT = 30000; |
| private static final long WARNING_DIALOG_TIMEOUT_DEFAULT = 6000; |
| private static final long MILLIS_PER_SECOND = 1000; |
| |
| private static final int[] VOLUME_STREAMS_TO_MUTE = { |
| AudioSystem.STREAM_RING, |
| AudioSystem.STREAM_MUSIC |
| }; |
| |
| // Tron Vars |
| private static final String DEMO_SESSION_COUNT = "retail_demo_session_count"; |
| private static final String DEMO_SESSION_DURATION = "retail_demo_session_duration"; |
| |
| boolean mDeviceInDemoMode = false; |
| int mCurrentUserId = UserHandle.USER_SYSTEM; |
| long mUserInactivityTimeout; |
| long mWarningDialogTimeout; |
| private ActivityManagerService mAms; |
| private ActivityManagerInternal mAmi; |
| private AudioManager mAudioManager; |
| private NotificationManager mNm; |
| private UserManager mUm; |
| private PowerManager mPm; |
| private PowerManager.WakeLock mWakeLock; |
| Handler mHandler; |
| private ServiceThread mHandlerThread; |
| private PendingIntent mResetDemoPendingIntent; |
| private CameraManager mCameraManager; |
| private String[] mCameraIdsWithFlash; |
| private Configuration mSystemUserConfiguration; |
| private PreloadAppsInstaller mPreloadAppsInstaller; |
| |
| final Object mActivityLock = new Object(); |
| // Whether the newly created demo user has interacted with the screen yet |
| @GuardedBy("mActivityLock") |
| boolean mUserUntouched; |
| @GuardedBy("mActivityLock") |
| long mFirstUserActivityTime; |
| @GuardedBy("mActivityLock") |
| long mLastUserActivityTime; |
| |
| private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!mDeviceInDemoMode) { |
| return; |
| } |
| switch (intent.getAction()) { |
| case Intent.ACTION_SCREEN_OFF: |
| mHandler.removeMessages(MSG_TURN_SCREEN_ON); |
| mHandler.sendEmptyMessageDelayed(MSG_TURN_SCREEN_ON, SCREEN_WAKEUP_DELAY); |
| break; |
| case ACTION_RESET_DEMO: |
| mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); |
| break; |
| } |
| } |
| }; |
| |
| final class MainHandler extends Handler { |
| |
| MainHandler(Looper looper) { |
| super(looper, null, true); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_TURN_SCREEN_ON: |
| if (mWakeLock.isHeld()) { |
| mWakeLock.release(); |
| } |
| mWakeLock.acquire(); |
| break; |
| case MSG_INACTIVITY_TIME_OUT: |
| if (isDemoLauncherDisabled()) { |
| Slog.i(TAG, "User inactivity timeout reached"); |
| showInactivityCountdownDialog(); |
| } |
| break; |
| case MSG_START_NEW_SESSION: |
| if (DEBUG) { |
| Slog.d(TAG, "Switching to a new demo user"); |
| } |
| removeMessages(MSG_START_NEW_SESSION); |
| removeMessages(MSG_INACTIVITY_TIME_OUT); |
| if (mCurrentUserId != UserHandle.USER_SYSTEM) { |
| logSessionDuration(); |
| } |
| final UserInfo demoUser = getUserManager().createUser(DEMO_USER_NAME, |
| UserInfo.FLAG_DEMO | UserInfo.FLAG_EPHEMERAL); |
| if (demoUser != null) { |
| setupDemoUser(demoUser); |
| getActivityManager().switchUser(demoUser.id); |
| } |
| break; |
| } |
| } |
| } |
| |
| private class SettingsObserver extends ContentObserver { |
| |
| private final static String KEY_USER_INACTIVITY_TIMEOUT = "user_inactivity_timeout_ms"; |
| private final static String KEY_WARNING_DIALOG_TIMEOUT = "warning_dialog_timeout_ms"; |
| |
| private final Uri mDeviceDemoModeUri = Settings.Global |
| .getUriFor(Settings.Global.DEVICE_DEMO_MODE); |
| private final Uri mDeviceProvisionedUri = Settings.Global |
| .getUriFor(Settings.Global.DEVICE_PROVISIONED); |
| private final Uri mRetailDemoConstantsUri = Settings.Global |
| .getUriFor(Settings.Global.RETAIL_DEMO_MODE_CONSTANTS); |
| |
| private final KeyValueListParser mParser = new KeyValueListParser(','); |
| |
| public SettingsObserver(Handler handler) { |
| super(handler); |
| } |
| |
| public void register() { |
| ContentResolver cr = getContext().getContentResolver(); |
| cr.registerContentObserver(mDeviceDemoModeUri, false, this, UserHandle.USER_SYSTEM); |
| cr.registerContentObserver(mDeviceProvisionedUri, false, this, UserHandle.USER_SYSTEM); |
| cr.registerContentObserver(mRetailDemoConstantsUri, false, this, |
| UserHandle.USER_SYSTEM); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri) { |
| if (mRetailDemoConstantsUri.equals(uri)) { |
| refreshTimeoutConstants(); |
| return; |
| } |
| if (mDeviceDemoModeUri.equals(uri)) { |
| mDeviceInDemoMode = UserManager.isDeviceInDemoMode(getContext()); |
| if (mDeviceInDemoMode) { |
| mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); |
| } else if (mWakeLock.isHeld()) { |
| mWakeLock.release(); |
| } |
| } |
| // If device is provisioned and left demo mode - run the cleanup in demo folder |
| if (!mDeviceInDemoMode && isDeviceProvisioned()) { |
| // Run on the bg thread to not block the fg thread |
| BackgroundThread.getHandler().post(new Runnable() { |
| @Override |
| public void run() { |
| if (!deletePreloadsFolderContents()) { |
| Slog.w(TAG, "Failed to delete preloads folder contents"); |
| } |
| } |
| }); |
| } |
| } |
| |
| private void refreshTimeoutConstants() { |
| try { |
| mParser.setString(Settings.Global.getString(getContext().getContentResolver(), |
| Settings.Global.RETAIL_DEMO_MODE_CONSTANTS)); |
| } catch (IllegalArgumentException exc) { |
| Slog.e(TAG, "Invalid string passed to KeyValueListParser"); |
| // Consuming the exception to fall back to default values. |
| } |
| mWarningDialogTimeout = mParser.getLong(KEY_WARNING_DIALOG_TIMEOUT, |
| WARNING_DIALOG_TIMEOUT_DEFAULT); |
| mUserInactivityTimeout = mParser.getLong(KEY_USER_INACTIVITY_TIMEOUT, |
| USER_INACTIVITY_TIMEOUT_DEFAULT); |
| mUserInactivityTimeout = Math.max(mUserInactivityTimeout, USER_INACTIVITY_TIMEOUT_MIN); |
| } |
| } |
| |
| private void showInactivityCountdownDialog() { |
| UserInactivityCountdownDialog dialog = new UserInactivityCountdownDialog(getContext(), |
| mWarningDialogTimeout, MILLIS_PER_SECOND); |
| dialog.setNegativeButtonClickListener(null); |
| dialog.setPositiveButtonClickListener(new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); |
| } |
| }); |
| dialog.setOnCountDownExpiredListener(new OnCountDownExpiredListener() { |
| @Override |
| public void onCountDownExpired() { |
| mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); |
| } |
| }); |
| dialog.show(); |
| } |
| |
| public RetailDemoModeService(Context context) { |
| super(context); |
| synchronized (mActivityLock) { |
| mFirstUserActivityTime = mLastUserActivityTime = SystemClock.uptimeMillis(); |
| } |
| mPreloadAppsInstaller = new PreloadAppsInstaller(); |
| } |
| |
| private Notification createResetNotification() { |
| return new Notification.Builder(getContext()) |
| .setContentTitle(getContext().getString(R.string.reset_retail_demo_mode_title)) |
| .setContentText(getContext().getString(R.string.reset_retail_demo_mode_text)) |
| .setOngoing(true) |
| .setSmallIcon(R.drawable.platlogo) |
| .setShowWhen(false) |
| .setVisibility(Notification.VISIBILITY_PUBLIC) |
| .setContentIntent(getResetDemoPendingIntent()) |
| .setColor(getContext().getColor(R.color.system_notification_accent_color)) |
| .build(); |
| } |
| |
| private PendingIntent getResetDemoPendingIntent() { |
| if (mResetDemoPendingIntent == null) { |
| Intent intent = new Intent(ACTION_RESET_DEMO); |
| mResetDemoPendingIntent = PendingIntent.getBroadcast(getContext(), 0, intent, 0); |
| } |
| return mResetDemoPendingIntent; |
| } |
| |
| boolean isDemoLauncherDisabled() { |
| IPackageManager pm = AppGlobals.getPackageManager(); |
| int enabledState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; |
| String demoLauncherComponent = getContext().getResources() |
| .getString(R.string.config_demoModeLauncherComponent); |
| try { |
| enabledState = pm.getComponentEnabledSetting( |
| ComponentName.unflattenFromString(demoLauncherComponent), |
| mCurrentUserId); |
| } catch (RemoteException exc) { |
| Slog.e(TAG, "Unable to talk to Package Manager", exc); |
| } |
| return enabledState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED; |
| } |
| |
| private void setupDemoUser(UserInfo userInfo) { |
| UserManager um = getUserManager(); |
| UserHandle user = UserHandle.of(userInfo.id); |
| um.setUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, true, user); |
| um.setUserRestriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, true, user); |
| um.setUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS, true, user); |
| um.setUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, true, user); |
| um.setUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS, true, user); |
| um.setUserRestriction(UserManager.DISALLOW_CONFIG_BLUETOOTH, true, user); |
| // Set this to false because the default is true on user creation |
| um.setUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, false, user); |
| // Disallow rebooting in safe mode - controlled by user 0 |
| getUserManager().setUserRestriction(UserManager.DISALLOW_SAFE_BOOT, true, |
| UserHandle.SYSTEM); |
| Settings.Secure.putIntForUser(getContext().getContentResolver(), |
| Settings.Secure.SKIP_FIRST_USE_HINTS, 1, userInfo.id); |
| Settings.Secure.putIntForUser(getContext().getContentResolver(), |
| Settings.Global.PACKAGE_VERIFIER_ENABLE, 0, userInfo.id); |
| |
| grantRuntimePermissionToCamera(user); |
| clearPrimaryCallLog(); |
| } |
| |
| private void grantRuntimePermissionToCamera(UserHandle user) { |
| final Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); |
| final PackageManager pm = getContext().getPackageManager(); |
| final ResolveInfo handler = pm.resolveActivityAsUser(cameraIntent, |
| PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, |
| user.getIdentifier()); |
| if (handler == null || handler.activityInfo == null) { |
| return; |
| } |
| try { |
| pm.grantRuntimePermission(handler.activityInfo.packageName, |
| Manifest.permission.ACCESS_FINE_LOCATION, user); |
| } catch (Exception e) { |
| // Ignore |
| } |
| } |
| |
| private void clearPrimaryCallLog() { |
| final ContentResolver resolver = getContext().getContentResolver(); |
| |
| // Deleting primary user call log so that it doesn't get copied to the new demo user |
| final Uri uri = CallLog.Calls.CONTENT_URI; |
| try { |
| resolver.delete(uri, null, null); |
| } catch (Exception e) { |
| Slog.w(TAG, "Deleting call log failed: " + e); |
| } |
| } |
| |
| void logSessionDuration() { |
| final int sessionDuration; |
| synchronized (mActivityLock) { |
| sessionDuration = (int) ((mLastUserActivityTime - mFirstUserActivityTime) / 1000); |
| } |
| MetricsLogger.histogram(getContext(), DEMO_SESSION_DURATION, sessionDuration); |
| } |
| |
| private ActivityManagerService getActivityManager() { |
| if (mAms == null) { |
| mAms = (ActivityManagerService) ActivityManagerNative.getDefault(); |
| } |
| return mAms; |
| } |
| |
| private UserManager getUserManager() { |
| if (mUm == null) { |
| mUm = getContext().getSystemService(UserManager.class); |
| } |
| return mUm; |
| } |
| |
| private AudioManager getAudioManager() { |
| if (mAudioManager == null) { |
| mAudioManager = getContext().getSystemService(AudioManager.class); |
| } |
| return mAudioManager; |
| } |
| |
| private boolean isDeviceProvisioned() { |
| return Settings.Global.getInt( |
| getContext().getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0; |
| } |
| |
| private boolean deletePreloadsFolderContents() { |
| final File dir = Environment.getDataPreloadsDirectory(); |
| Slog.i(TAG, "Deleting contents of " + dir); |
| return FileUtils.deleteContents(dir); |
| } |
| |
| private void registerBroadcastReceiver() { |
| final IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_SCREEN_OFF); |
| filter.addAction(ACTION_RESET_DEMO); |
| getContext().registerReceiver(mBroadcastReceiver, filter); |
| } |
| |
| private String[] getCameraIdsWithFlash() { |
| ArrayList<String> cameraIdsList = new ArrayList<String>(); |
| try { |
| for (String cameraId : mCameraManager.getCameraIdList()) { |
| CameraCharacteristics c = mCameraManager.getCameraCharacteristics(cameraId); |
| if (Boolean.TRUE.equals(c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE))) { |
| cameraIdsList.add(cameraId); |
| } |
| } |
| } catch (CameraAccessException e) { |
| Slog.e(TAG, "Unable to access camera while getting camera id list", e); |
| } |
| return cameraIdsList.toArray(new String[cameraIdsList.size()]); |
| } |
| |
| private void turnOffAllFlashLights() { |
| for (String cameraId : mCameraIdsWithFlash) { |
| try { |
| mCameraManager.setTorchMode(cameraId, false); |
| } catch (CameraAccessException e) { |
| Slog.e(TAG, "Unable to access camera " + cameraId + " while turning off flash", e); |
| } |
| } |
| } |
| |
| private void muteVolumeStreams() { |
| for (int stream : VOLUME_STREAMS_TO_MUTE) { |
| getAudioManager().setStreamVolume(stream, getAudioManager().getStreamMinVolume(stream), |
| 0); |
| } |
| } |
| |
| private Configuration getSystemUsersConfiguration() { |
| if (mSystemUserConfiguration == null) { |
| Settings.System.getConfiguration(getContext().getContentResolver(), |
| mSystemUserConfiguration = new Configuration()); |
| } |
| return mSystemUserConfiguration; |
| } |
| |
| @Override |
| public void onStart() { |
| if (DEBUG) { |
| Slog.d(TAG, "Service starting up"); |
| } |
| mHandlerThread = new ServiceThread(TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, |
| false); |
| mHandlerThread.start(); |
| mHandler = new MainHandler(mHandlerThread.getLooper()); |
| publishLocalService(RetailDemoModeServiceInternal.class, mLocalService); |
| } |
| |
| @Override |
| public void onBootPhase(int bootPhase) { |
| if (bootPhase != PHASE_THIRD_PARTY_APPS_CAN_START) { |
| return; |
| } |
| mPm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); |
| mAmi = LocalServices.getService(ActivityManagerInternal.class); |
| mWakeLock = mPm |
| .newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); |
| mNm = NotificationManager.from(getContext()); |
| mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); |
| mCameraIdsWithFlash = getCameraIdsWithFlash(); |
| |
| if (UserManager.isDeviceInDemoMode(getContext())) { |
| mDeviceInDemoMode = true; |
| mHandler.sendEmptyMessage(MSG_START_NEW_SESSION); |
| } |
| SettingsObserver settingsObserver = new SettingsObserver(mHandler); |
| settingsObserver.register(); |
| settingsObserver.refreshTimeoutConstants(); |
| registerBroadcastReceiver(); |
| } |
| |
| @Override |
| public void onSwitchUser(int userId) { |
| if (!mDeviceInDemoMode) { |
| return; |
| } |
| if (DEBUG) { |
| Slog.d(TAG, "onSwitchUser: " + userId); |
| } |
| final UserInfo ui = getUserManager().getUserInfo(userId); |
| if (!ui.isDemo()) { |
| Slog.wtf(TAG, "Should not allow switch to non-demo user in demo mode"); |
| return; |
| } |
| if (!mWakeLock.isHeld()) { |
| mWakeLock.acquire(); |
| } |
| mCurrentUserId = userId; |
| mAmi.updatePersistentConfigurationForUser(getSystemUsersConfiguration(), userId); |
| turnOffAllFlashLights(); |
| muteVolumeStreams(); |
| // Disable lock screen for demo users. |
| LockPatternUtils lockPatternUtils = new LockPatternUtils(getContext()); |
| lockPatternUtils.setLockScreenDisabled(true, userId); |
| mNm.notifyAsUser(TAG, 1, createResetNotification(), UserHandle.of(userId)); |
| |
| synchronized (mActivityLock) { |
| mUserUntouched = true; |
| } |
| MetricsLogger.count(getContext(), DEMO_SESSION_COUNT, 1); |
| mHandler.removeMessages(MSG_INACTIVITY_TIME_OUT); |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| mPreloadAppsInstaller.installApps(userId); |
| } |
| }); |
| } |
| |
| private RetailDemoModeServiceInternal mLocalService = new RetailDemoModeServiceInternal() { |
| private static final long USER_ACTIVITY_DEBOUNCE_TIME = 2000; |
| |
| @Override |
| public void onUserActivity() { |
| if (!mDeviceInDemoMode) { |
| return; |
| } |
| long timeOfActivity = SystemClock.uptimeMillis(); |
| synchronized (mActivityLock) { |
| if (timeOfActivity < mLastUserActivityTime + USER_ACTIVITY_DEBOUNCE_TIME) { |
| return; |
| } |
| mLastUserActivityTime = timeOfActivity; |
| if (mUserUntouched && isDemoLauncherDisabled()) { |
| Slog.d(TAG, "retail_demo first touch"); |
| mUserUntouched = false; |
| mFirstUserActivityTime = timeOfActivity; |
| } |
| } |
| mHandler.removeMessages(MSG_INACTIVITY_TIME_OUT); |
| mHandler.sendEmptyMessageDelayed(MSG_INACTIVITY_TIME_OUT, mUserInactivityTimeout); |
| } |
| }; |
| } |