From 1bec0c472b961e3fe6779f310b601211db9003a6 Mon Sep 17 00:00:00 2001 From: Vadim Tryshev Date: Wed, 12 Aug 2015 12:02:22 -0700 Subject: [PATCH] Checking for unlauncheable apps. When loading apps from prefs, detecting apps that cannot be launched (for example, were uninstalled) and not adding them to the shelf. Also watching for apps that become unlauncheable while the shelf is shown, and removing them as it happens. Bug: 22694757 Change-Id: Iab9494f0480e9bf4438b72a94041002be010334f --- .../statusbar/phone/NavigationBarApps.java | 136 ++++++++------ .../phone/NavigationBarAppsModel.java | 72 ++++++- .../phone/NavigationBarAppsModelTest.java | 175 +++++++++++++++++- 3 files changed, 329 insertions(+), 54 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java index d74c5b01307b1..cb5c1257a8317 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java @@ -20,7 +20,6 @@ import android.animation.LayoutTransition; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.AppGlobals; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; @@ -29,13 +28,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; -import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.AttributeSet; @@ -49,6 +45,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.android.internal.content.PackageMonitor; import com.android.systemui.R; import java.util.List; @@ -75,6 +72,8 @@ class NavigationBarApps extends LinearLayout { private final PackageManager mPackageManager; private final UserManager mUserManager; private final LayoutInflater mLayoutInflater; + private final AppPackageMonitor mAppPackageMonitor; + // This view has two roles: // 1) If the drag started outside the pinned apps list, it is a placeholder icon with a null @@ -106,6 +105,7 @@ class NavigationBarApps extends LinearLayout { mPackageManager = context.getPackageManager(); mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mLayoutInflater = LayoutInflater.from(context); + mAppPackageMonitor = new AppPackageMonitor(); // Dragging an icon removes and adds back the dragged icon. Use the layout transitions to // trigger animation. By default all transitions animate, so turn off the unneeded ones. @@ -121,6 +121,76 @@ class NavigationBarApps extends LinearLayout { setLayoutTransition(transition); } + // Monitor that catches events like "app uninstalled". + private class AppPackageMonitor extends PackageMonitor { + @Override + public void onPackageRemoved(String packageName, int uid) { + postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); + super.onPackageRemoved(packageName, uid); + } + + @Override + public void onPackageModified(String packageName) { + postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); + super.onPackageModified(packageName); + } + + @Override + public void onPackagesAvailable(String[] packages) { + if (isReplacing()) { + UserHandle user = new UserHandle(getChangingUserId()); + + for (String packageName : packages) { + postRemoveIfUnlauncheable(packageName, user); + } + } + super.onPackagesAvailable(packages); + } + + @Override + public void onPackagesUnavailable(String[] packages) { + if (!isReplacing()) { + UserHandle user = new UserHandle(getChangingUserId()); + + for (String packageName : packages) { + postRemoveIfUnlauncheable(packageName, user); + } + } + super.onPackagesUnavailable(packages); + } + } + + private void postRemoveIfUnlauncheable(final String packageName, final UserHandle user) { + // This method doesn't necessarily get called in the main thread. Redirect the call into + // the main thread. + post(new Runnable() { + @Override + public void run() { + if (!isAttachedToWindow()) return; + removeIfUnlauncheable(packageName, user); + } + }); + } + + private void removeIfUnlauncheable(String packageName, UserHandle user) { + long appUserSerialNumber = mUserManager.getSerialNumberForUser(user); + + // Remove icons for all apps that match a package that perhaps became unlauncheable. + for(int i = sAppsModel.getAppCount() - 1; i >= 0; --i) { + AppInfo appInfo = sAppsModel.getApp(i); + if (appInfo.getUserSerialNumber() != appUserSerialNumber) continue; + + ComponentName appComponentName = appInfo.getComponentName(); + if (!appComponentName.getPackageName().equals(packageName)) continue; + + if (sAppsModel.buildAppLaunchIntent(appComponentName, user) != null) continue; + + removeViewAt(i); + sAppsModel.removeApp(i); + sAppsModel.savePrefs(); + } + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -145,12 +215,15 @@ class NavigationBarApps extends LinearLayout { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(mBroadcastReceiver, filter); + + mAppPackageMonitor.register(mContext, null, UserHandle.ALL, true); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mContext.unregisterReceiver(mBroadcastReceiver); + mAppPackageMonitor.unregister(); } /** @@ -470,7 +543,6 @@ class NavigationBarApps extends LinearLayout { ComponentName component = appInfo.getComponentName(); long appUserSerialNumber = appInfo.getUserSerialNumber(); - UserHandle appUser = mUserManager.getUserForSerialNumber(appUserSerialNumber); if (appUser == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); @@ -478,7 +550,12 @@ class NavigationBarApps extends LinearLayout { " because its user doesn't exist."); return; } - int appUserId = appUser.getIdentifier(); + + Intent launchIntent = sAppsModel.buildAppLaunchIntent(component, appUser); + if (launchIntent == null) { + Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + return; + } // Play a scale-up animation while launching the activity. // TODO: Consider playing a different animation, or no animation, if the activity is @@ -489,54 +566,9 @@ class NavigationBarApps extends LinearLayout { ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()); Bundle optsBundle = opts.toBundle(); - - // Launch the activity. This code is based on LauncherAppsService.startActivityAsUser code. - Intent launchIntent = new Intent(Intent.ACTION_MAIN); - launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); launchIntent.setSourceBounds(sourceBounds); - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - launchIntent.setPackage(component.getPackageName()); - IPackageManager pm = AppGlobals.getPackageManager(); - try { - ActivityInfo info = pm.getActivityInfo(component, 0, appUserId); - if (info == null) { - Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Can't start activity " + component + " because it's not installed."); - return; - } - - if (!info.exported) { - Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Can't start activity " + component + " because it doesn't have 'exported' attribute."); - return; - } - } catch (RemoteException e) { - Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Failed to get activity info for " + component, e); - return; - } - - // Check that the component actually has Intent.CATEGORY_LAUCNCHER - // as calling startActivityAsUser ignores the category and just - // resolves based on the component if present. - List apps = getContext().getPackageManager().queryIntentActivitiesAsUser(launchIntent, - 0 /* flags */, appUserId); - final int size = apps.size(); - for (int i = 0; i < size; ++i) { - ActivityInfo activityInfo = apps.get(i).activityInfo; - if (activityInfo.packageName.equals(component.getPackageName()) && - activityInfo.name.equals(component.getClassName())) { - // Found an activity with category launcher that matches - // this component so ok to launch. - launchIntent.setComponent(component); - mContext.startActivityAsUser(launchIntent, optsBundle, appUser); - return; - } - } - - Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Attempt to launch activity without category Intent.CATEGORY_LAUNCHER " + component); + mContext.startActivityAsUser(launchIntent, optsBundle, appUser); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java index b8764cf40efb0..c4c31fd6bf80b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java @@ -16,16 +16,23 @@ package com.android.systemui.statusbar.phone; +import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -94,6 +101,58 @@ class NavigationBarAppsModel { } } + @VisibleForTesting + protected IPackageManager getPackageManager() { + return AppGlobals.getPackageManager(); + } + + // Returns a launch intent for a given component, or null if the component is unlauncheable. + public Intent buildAppLaunchIntent(ComponentName component, UserHandle appUser) { + int appUserId = appUser.getIdentifier(); + + // This code is based on LauncherAppsService.startActivityAsUser code. + Intent launchIntent = new Intent(Intent.ACTION_MAIN); + launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + launchIntent.setPackage(component.getPackageName()); + + try { + ActivityInfo info = getPackageManager().getActivityInfo(component, 0, appUserId); + if (info == null) { + Log.e(TAG, "Activity " + component + " is not installed."); + return null; + } + + if (!info.exported) { + Log.e(TAG, "Activity " + component + " doesn't have 'exported' attribute."); + return null; + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to get activity info for " + component, e); + return null; + } + + // Check that the component actually has Intent.CATEGORY_LAUNCHER + // as calling startActivityAsUser ignores the category and just + // resolves based on the component if present. + List apps = mContext.getPackageManager().queryIntentActivitiesAsUser(launchIntent, + 0 /* flags */, appUserId); + final int size = apps.size(); + for (int i = 0; i < size; ++i) { + ActivityInfo activityInfo = apps.get(i).activityInfo; + if (activityInfo.packageName.equals(component.getPackageName()) && + activityInfo.name.equals(component.getClassName())) { + // Found an activity with category launcher that matches + // this component so ok to launch. + launchIntent.setComponent(component); + return launchIntent; + } + } + + Log.e(TAG, "Activity doesn't have category Intent.CATEGORY_LAUNCHER " + component); + return null; + } + /** * Reinitializes the model for a new user. */ @@ -199,6 +258,10 @@ class NavigationBarAppsModel { /** Loads the list of apps from SharedPreferences. */ private void loadAppsFromPrefs() { + UserManager mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + + boolean hadUnlauncheableApps = false; + int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1); for (int i = 0; i < appCount; i++) { String prefValue = mPrefs.getString(prefNameForApp(i), null); @@ -214,8 +277,15 @@ class NavigationBarAppsModel { // Couldn't find the saved state. Just skip this item. continue; } - mApps.add(new AppInfo(componentName, userSerialNumber)); + UserHandle appUser = mUserManager.getUserForSerialNumber(userSerialNumber); + if (appUser != null && buildAppLaunchIntent(componentName, appUser) != null) { + mApps.add(new AppInfo(componentName, userSerialNumber)); + } else { + hadUnlauncheableApps = true; + } } + + if (hadUnlauncheableApps) savePrefs(); } /** Adds the first few apps from the owner profile. Used for demo purposes. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java index 62213ab517030..4d0e28bb6d986 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java @@ -16,21 +16,26 @@ package com.android.systemui.statusbar.phone; +import org.mockito.InOrder; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.test.AndroidTestCase; @@ -45,6 +50,7 @@ import java.util.Map; /** Tests for the data model for the navigation bar app icons. */ public class NavigationBarAppsModelTest extends AndroidTestCase { private PackageManager mMockPackageManager; + private IPackageManager mMockIPackageManager; private SharedPreferences mMockPrefs; private SharedPreferences.Editor mMockEdit; private UserManager mMockUserManager; @@ -61,6 +67,7 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { final Context context = mock(Context.class); mMockPackageManager = mock(PackageManager.class); + mMockIPackageManager = mock(IPackageManager.class); mMockPrefs = mock(SharedPreferences.class); mMockEdit = mock(SharedPreferences.Editor.class); mMockUserManager = mock(UserManager.class); @@ -78,8 +85,71 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.edit()).thenReturn(mMockEdit); when(mMockUserManager.getSerialNumberForUser(new UserHandle(2))).thenReturn(22L); + when(mMockUserManager.getUserForSerialNumber(45L)).thenReturn(new UserHandle(4)); + when(mMockUserManager.getUserForSerialNumber(239L)).thenReturn(new UserHandle(5)); - mModel = new NavigationBarAppsModel(context); + mModel = new NavigationBarAppsModel(context) { + @Override + protected IPackageManager getPackageManager() { + return mMockIPackageManager; + } + }; + } + + /** Tests buildAppLaunchIntent(). */ + public void testBuildAppLaunchIntent() { + ActivityInfo mockNonExportedActivityInfo = new ActivityInfo(); + mockNonExportedActivityInfo.exported = false; + ActivityInfo mockExportedActivityInfo = new ActivityInfo(); + mockExportedActivityInfo.exported = true; + try { + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package1", "class1"), 0, 4)). + thenReturn(mockNonExportedActivityInfo); + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package2", "class2"), 0, 5)). + thenThrow(new RemoteException()); + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package3", "class3"), 0, 6)). + thenReturn(mockExportedActivityInfo); + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package4", "class4"), 0, 7)). + thenReturn(mockExportedActivityInfo); + } catch (RemoteException e) { + fail("RemoteException can't happen in the test, but it happened."); + } + + // Assume some installed activities. + ActivityInfo ai0 = new ActivityInfo(); + ai0.packageName = "package0"; + ai0.name = "class0"; + ActivityInfo ai1 = new ActivityInfo(); + ai1.packageName = "package4"; + ai1.name = "class4"; + ResolveInfo ri0 = new ResolveInfo(); + ri0.activityInfo = ai0; + ResolveInfo ri1 = new ResolveInfo(); + ri1.activityInfo = ai1; + when(mMockPackageManager + .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) + .thenReturn(Arrays.asList(ri0, ri1)); + + // Unlauncheable (for various reasons) apps. + assertEquals(null, mModel.buildAppLaunchIntent( + new ComponentName("package0", "class0"), new UserHandle(3))); + assertEquals(null, mModel.buildAppLaunchIntent( + new ComponentName("package1", "class1"), new UserHandle(4))); + assertEquals(null, mModel.buildAppLaunchIntent( + new ComponentName("package2", "class2"), new UserHandle(5))); + assertEquals(null, mModel.buildAppLaunchIntent( + new ComponentName("package3", "class3"), new UserHandle(6))); + + // A launcheable app. + Intent intent = mModel.buildAppLaunchIntent( + new ComponentName("package4", "class4"), new UserHandle(7)); + assertNotNull(intent); + assertEquals(new ComponentName("package4", "class4"), intent.getComponent()); + assertEquals("package4", intent.getPackage()); } /** Initializes the model from SharedPreferences for a few app activites. */ @@ -93,6 +163,39 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.getString("22|app_2", null)).thenReturn("package2/class2"); when(mMockPrefs.getLong("22|app_user_2", -1)).thenReturn(239L); + ActivityInfo mockActivityInfo = new ActivityInfo(); + mockActivityInfo.exported = true; + try { + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package1", "class1"), 0, 4)).thenReturn(mockActivityInfo); + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package2", "class2"), 0, 5)).thenReturn(mockActivityInfo); + } catch (RemoteException e) { + fail("RemoteException can't happen in the test, but it happened."); + } + + // Assume some installed activities. + ActivityInfo ai0 = new ActivityInfo(); + ai0.packageName = "package0"; + ai0.name = "class0"; + ActivityInfo ai1 = new ActivityInfo(); + ai1.packageName = "package1"; + ai1.name = "class1"; + ActivityInfo ai2 = new ActivityInfo(); + ai2.packageName = "package2"; + ai2.name = "class2"; + ResolveInfo ri0 = new ResolveInfo(); + ri0.activityInfo = ai0; + ResolveInfo ri1 = new ResolveInfo(); + ri1.activityInfo = ai1; + ResolveInfo ri2 = new ResolveInfo(); + ri2.activityInfo = ai2; + when(mMockPackageManager + .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) + .thenReturn(Arrays.asList(ri0, ri1, ri2)); + mModel.setCurrentUser(2); } @@ -133,6 +236,15 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { assertEquals(22L, mModel.getApp(0).getUserSerialNumber()); assertEquals("package2/class2", mModel.getApp(1).getComponentName().flattenToString()); assertEquals(22L, mModel.getApp(1).getUserSerialNumber()); + InOrder order = inOrder(mMockEdit); + order.verify(mMockEdit).apply(); + order.verify(mMockEdit).putInt("22|app_count", 2); + order.verify(mMockEdit).putString("22|app_0", "package1/class1"); + order.verify(mMockEdit).putLong("22|app_user_0", 22L); + order.verify(mMockEdit).putString("22|app_1", "package2/class2"); + order.verify(mMockEdit).putLong("22|app_user_1", 22L); + order.verify(mMockEdit).apply(); + verifyNoMoreInteractions(mMockEdit); } /** Tests initializing the model if one of the prefs is missing. */ @@ -145,11 +257,72 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // But assume one pref is missing. when(mMockPrefs.getString("22|app_1", null)).thenReturn(null); + ActivityInfo mockActivityInfo = new ActivityInfo(); + mockActivityInfo.exported = true; + try { + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); + } catch (RemoteException e) { + fail("RemoteException can't happen in the test, but it happened."); + } + + ActivityInfo ai0 = new ActivityInfo(); + ai0.packageName = "package0"; + ai0.name = "class0"; + ResolveInfo ri0 = new ResolveInfo(); + ri0.activityInfo = ai0; + when(mMockPackageManager + .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) + .thenReturn(Arrays.asList(ri0)); + // Initializing the model should load from prefs and skip the missing one. mModel.setCurrentUser(2); assertEquals(1, mModel.getAppCount()); assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); + verifyNoMoreInteractions(mMockEdit); + } + + /** Tests initializing the model if one of the apps is unlauncheable. */ + public void testInitializeWithUnlauncheableApp() { + // Assume two apps are nominally stored. + when(mMockPrefs.getInt("22|app_count", -1)).thenReturn(2); + when(mMockPrefs.getString("22|app_0", null)).thenReturn("package0/class0"); + when(mMockPrefs.getLong("22|app_user_0", -1)).thenReturn(239L); + when(mMockPrefs.getString("22|app_1", null)).thenReturn("package1/class1"); + when(mMockPrefs.getLong("22|app_user_1", -1)).thenReturn(45L); + + ActivityInfo mockActivityInfo = new ActivityInfo(); + mockActivityInfo.exported = true; + try { + when(mMockIPackageManager.getActivityInfo( + new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); + } catch (RemoteException e) { + fail("RemoteException can't happen in the test, but it happened."); + } + + ActivityInfo ai0 = new ActivityInfo(); + ai0.packageName = "package0"; + ai0.name = "class0"; + ResolveInfo ri0 = new ResolveInfo(); + ri0.activityInfo = ai0; + when(mMockPackageManager + .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) + .thenReturn(Arrays.asList(ri0)); + + // Initializing the model should load from prefs and skip the unlauncheable one. + mModel.setCurrentUser(2); + assertEquals(1, mModel.getAppCount()); + assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); + assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); + + // Once an unlauncheable app is detected, the model should save all apps excluding the + // unlauncheable one. + verify(mMockEdit).putInt("22|app_count", 1); + verify(mMockEdit).putString("22|app_0", "package0/class0"); + verify(mMockEdit).putLong("22|app_user_0", 239L); + verify(mMockEdit).apply(); + verifyNoMoreInteractions(mMockEdit); } /** Tests saving the model to SharedPreferences. */