summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml5
-rw-r--r--build.gradle2
-rw-r--r--res/xml/backupscheme.xml7
-rw-r--r--src/com/android/launcher3/AutoInstallsLayout.java2
-rw-r--r--src/com/android/launcher3/Hotseat.java59
-rw-r--r--src/com/android/launcher3/Launcher.java33
-rw-r--r--src/com/android/launcher3/LauncherAppState.java7
-rw-r--r--src/com/android/launcher3/LauncherAppWidgetHostView.java2
-rw-r--r--src/com/android/launcher3/LauncherBackupAgent.java133
-rw-r--r--src/com/android/launcher3/LauncherModel.java27
-rw-r--r--src/com/android/launcher3/LauncherProvider.java366
-rw-r--r--src/com/android/launcher3/LauncherSettings.java28
-rw-r--r--src/com/android/launcher3/Utilities.java15
-rw-r--r--src/com/android/launcher3/config/FeatureFlags.java12
-rw-r--r--src/com/android/launcher3/config/ProviderConfig.java2
-rw-r--r--src/com/android/launcher3/dynamicui/ColorExtractionService.java14
-rw-r--r--src/com/android/launcher3/dynamicui/ExtractedColors.java86
-rw-r--r--src/com/android/launcher3/dynamicui/ExtractionUtils.java44
-rw-r--r--src/com/android/launcher3/folder/Folder.java9
-rw-r--r--src/com/android/launcher3/logging/FileLog.java216
-rw-r--r--tests/src/com/android/launcher3/LauncherBackupAgentTest.java74
-rw-r--r--tests/src/com/android/launcher3/logging/FileLogTest.java77
-rw-r--r--tests/src/com/android/launcher3/util/TestLauncherProvider.java12
23 files changed, 913 insertions, 319 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e7b703cab..9192764b5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,8 +53,9 @@
<uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
<application
- android:allowBackup="@bool/enable_backup"
- android:backupAgent="com.android.launcher3.LauncherBackupAgentHelper"
+ android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+ android:fullBackupOnly="true"
+ android:fullBackupContent="@xml/backupscheme"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher_home"
android:label="@string/app_name"
diff --git a/build.gradle b/build.gradle
index 4df406363..d777e95c8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.5.0'
+ classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.7.0'
}
}
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
new file mode 100644
index 000000000..7e833a0ca
--- /dev/null
+++ b/res/xml/backupscheme.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <include domain="database" path="launcher.db" />
+ <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
+
+</full-backup-content> \ No newline at end of file
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 0d71a0c6c..a04c5570a 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -630,7 +630,7 @@ public class AutoInstallsLayout {
copyInteger(myValues, childValues, Favorites.CELLY);
addedId = folderItems.get(0);
- mDb.update(LauncherProvider.TABLE_FAVORITES, childValues,
+ mDb.update(Favorites.TABLE_NAME, childValues,
Favorites._ID + "=" + addedId, null);
}
}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index bb70be697..7e1ecf5af 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,8 +16,12 @@
package com.android.launcher3;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -27,12 +31,13 @@ import android.view.ViewDebug;
import android.widget.FrameLayout;
import android.widget.TextView;
+import com.android.launcher3.dynamicui.ExtractedColors;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
public class Hotseat extends FrameLayout
- implements UserEventDispatcher.LaunchSourceProvider{
+ implements UserEventDispatcher.LaunchSourceProvider, Insettable {
private CellLayout mContent;
@@ -44,6 +49,14 @@ public class Hotseat extends FrameLayout
@ViewDebug.ExportedProperty(category = "launcher")
private final boolean mHasVerticalHotseat;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private Rect mInsets = new Rect();
+
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private int mBackgroundColor;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private ColorDrawable mBackground;
+
public Hotseat(Context context) {
this(context, null);
}
@@ -56,6 +69,8 @@ public class Hotseat extends FrameLayout
super(context, attrs, defStyle);
mLauncher = (Launcher) context;
mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
+ mBackground = new ColorDrawable();
+ setBackground(mBackground);
}
public CellLayout getLayout() {
@@ -166,4 +181,46 @@ public class Hotseat extends FrameLayout
target.gridY = info.cellY;
targetParent.containerType = LauncherLogProto.HOTSEAT;
}
+
+ //Overridden so that the background color extends behind the navigation buttons.
+ @Override
+ public void setInsets(Rect insets) {
+ int rightInset = insets.right - mInsets.right;
+ int bottomInset = insets.bottom - mInsets.bottom;
+ mInsets.set(insets);
+ LayoutParams lp = (LayoutParams) getLayoutParams();
+ if (mHasVerticalHotseat) {
+ setPadding(getPaddingLeft(), getPaddingTop(),
+ getPaddingRight() + rightInset, getPaddingBottom());
+ if (lp.width != LayoutParams.MATCH_PARENT && lp.width != LayoutParams.WRAP_CONTENT) {
+ lp.width += rightInset;
+ }
+ } else {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+ getPaddingBottom() + bottomInset);
+ if (lp.height != LayoutParams.MATCH_PARENT && lp.height != LayoutParams.WRAP_CONTENT) {
+ lp.height += bottomInset;
+ }
+ }
+ }
+
+ public void updateColor(ExtractedColors extractedColors, boolean animate) {
+ if (!mHasVerticalHotseat) {
+ int color = extractedColors.getColor(ExtractedColors.HOTSEAT_INDEX, Color.TRANSPARENT);
+ if (!animate) {
+ setBackgroundColor(color);
+ } else {
+ ValueAnimator animator = ValueAnimator.ofInt(mBackgroundColor, color);
+ animator.setEvaluator(new ArgbEvaluator());
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mBackground.setColor((Integer) animation.getAnimatedValue());
+ }
+ });
+ animator.start();
+ }
+ mBackgroundColor = color;
+ }
+ }
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b2f96d099..eacf72aa1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -114,6 +114,7 @@ import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.TestingUtils;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.ViewOnDrawExecutor;
@@ -123,11 +124,9 @@ import com.android.launcher3.widget.WidgetsContainerView;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -307,11 +306,6 @@ public class Launcher extends Activity
private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>();
private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false;
- private static final ArrayList<String> sDumpLogs = new ArrayList<String>();
- private static final Date sDateStamp = new Date();
- private static final DateFormat sDateFormat =
- DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
-
// We only want to get the SharedPreferences once since it does an FS stat each time we get
// it from the context.
private SharedPreferences mSharedPrefs;
@@ -502,9 +496,10 @@ public class Launcher extends Activity
}
private void loadExtractedColorsAndColorItems() {
- if (mExtractedColors != null) {
+ // TODO: do this in pre-N as well, once the extraction part is complete.
+ if (mExtractedColors != null && Utilities.isNycOrAbove()) {
mExtractedColors.load(this);
- // TODO: pass mExtractedColors to interested items such as hotseat.
+ mHotseat.updateColor(mExtractedColors, !mPaused);
}
}
@@ -3976,7 +3971,7 @@ public class Launcher extends Activity
// Verify that we own the widget
if (appWidgetInfo == null) {
- Log.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
+ FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
deleteWidgetInfo(item);
return;
}
@@ -4649,12 +4644,10 @@ public class Launcher extends Activity
}
}
- synchronized (sDumpLogs) {
- writer.println();
- writer.println(prefix + "Debug logs");
- for (String log : sDumpLogs) {
- writer.println(prefix + " " + log);
- }
+ try {
+ FileLog.flushAll(writer);
+ } catch (Exception e) {
+ // Ignore
}
if (mLauncherCallbacks != null) {
@@ -4662,14 +4655,6 @@ public class Launcher extends Activity
}
}
- public static void addDumpLog(String tag, String log) {
- Log.d(tag, log);
- synchronized(sDumpLogs) {
- sDateStamp.setTime(System.currentTimeMillis());
- sDumpLogs.add(sDateFormat.format(sDateStamp) + ": " + tag + ", " + log);
- }
- }
-
public static CustomAppWidget getCustomAppWidget(String name) {
return sCustomAppWidgets.get(name);
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index f84e4b5b4..9d889e09a 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -27,9 +27,9 @@ import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.compat.PackageInstallerCompat;
import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dynamicui.ExtractionUtils;
import com.android.launcher3.util.ConfigMonitor;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.TestingUtils;
import com.android.launcher3.util.Thunk;
@@ -79,6 +79,7 @@ public class LauncherAppState {
// is the first component to get created. Initializing application context here ensures
// that LauncherAppState always exists in the main process.
sContext = provider.getContext().getApplicationContext();
+ FileLog.setDir(sContext.getFilesDir());
}
private LauncherAppState() {
@@ -184,8 +185,4 @@ public class LauncherAppState {
public InvariantDeviceProfile getInvariantDeviceProfile() {
return mInvariantDeviceProfile;
}
-
- public static boolean isDogfoodBuild() {
- return FeatureFlags.IS_ALPHA_BUILD || FeatureFlags.IS_DEV_BUILD;
- }
}
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
index 570607ec7..28557d0a5 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -256,7 +256,7 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
- dispatchChildFocus(focused != null);
+ dispatchChildFocus(mChildrenFocused && focused != null);
if (focused != null) {
focused.setFocusableInTouchMode(false);
}
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
new file mode 100644
index 000000000..b2f5c5717
--- /dev/null
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -0,0 +1,133 @@
+package com.android.launcher3;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.ParcelFileDescriptor;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.logging.FileLog;
+
+import java.io.InvalidObjectException;
+
+public class LauncherBackupAgent extends BackupAgent {
+
+ private static final String TAG = "LauncherBackupAgent";
+
+ private static final String INFO_COLUMN_NAME = "name";
+ private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
+
+ @Override
+ public void onRestore(
+ BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
+ // Doesn't do incremental backup/restore
+ }
+
+ @Override
+ public void onBackup(
+ ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
+ // Doesn't do incremental backup/restore
+ }
+
+ @Override
+ public void onRestoreFinished() {
+ DatabaseHelper helper = new DatabaseHelper(this, null, LauncherFiles.LAUNCHER_DB);
+
+ if (!sanitizeDBSafely(helper)) {
+ helper.createEmptyDB(helper.getWritableDatabase());
+ }
+
+ try {
+ // Flush all logs before the process is killed.
+ FileLog.flushAll(null);
+ } catch (Exception e) { }
+ }
+
+ private boolean sanitizeDBSafely(DatabaseHelper helper) {
+ SQLiteDatabase db = helper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ sanitizeDB(helper, db);
+ db.setTransactionSuccessful();
+ return true;
+ } catch (Exception e) {
+ FileLog.e(TAG, "Failed to verify db", e);
+ return false;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Makes the following changes in the provider DB.
+ * 1. Removes all entries belonging to a managed profile as managed profiles
+ * cannot be restored.
+ * 2. Marks all entries as restored. The flags are updated during first load or as
+ * the restored apps get installed.
+ * 3. If the user serial for primary profile is different than that of the previous device,
+ * update the entries to the new profile id.
+ */
+ private void sanitizeDB(DatabaseHelper helper, SQLiteDatabase db) throws Exception {
+ long oldProfileId = getDefaultProfileId(db);
+ // Delete all entries which do not belong to the main user
+ int itemsDeleted = db.delete(
+ Favorites.TABLE_NAME, "profileId != ?", new String[]{Long.toString(oldProfileId)});
+ if (itemsDeleted > 0) {
+ FileLog.d(TAG, itemsDeleted + " items belonging to a managed profile, were deleted");
+ }
+
+ // Mark all items as restored.
+ ContentValues values = new ContentValues();
+ values.put(Favorites.RESTORED, 1);
+ db.update(Favorites.TABLE_NAME, values, null, null);
+
+ // Mark widgets with appropriate restore flag
+ values.put(Favorites.RESTORED,
+ LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
+ LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
+ LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
+ db.update(Favorites.TABLE_NAME, values, "itemType = ?",
+ new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
+
+ long myProfileId = helper.getDefaultUserSerial();
+ if (Utilities.longCompare(oldProfileId, myProfileId) != 0) {
+ FileLog.d(TAG, "Changing primary user id from " + oldProfileId + " to " + myProfileId);
+ migrateProfileId(db, myProfileId);
+ }
+ }
+
+ /**
+ * Updates profile id of all entries and changes the default value for the column.
+ */
+ protected void migrateProfileId(SQLiteDatabase db, long newProfileId) {
+ // Update existing entries.
+ ContentValues values = new ContentValues();
+ values.put(Favorites.PROFILE_ID, newProfileId);
+ db.update(Favorites.TABLE_NAME, values, null, null);
+
+ // Change default value of the column.
+ db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;");
+ Favorites.addTableToDb(db, newProfileId, false);
+ db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;");
+ db.execSQL("DROP TABLE favorites_old;");
+ }
+
+ /**
+ * Returns the profile id for used in the favorites table of the provided db.
+ */
+ protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
+ try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+ int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
+ while (c.moveToNext()) {
+ if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
+ return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE));
+ }
+ }
+ throw new InvalidObjectException("Table does not have a profile id column");
+ }
+ }
+}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 884685c8a..2fd12fd57 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -60,6 +60,7 @@ import com.android.launcher3.model.GridSizeMigrationTask;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.CursorIconInfo;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.LongArrayMap;
import com.android.launcher3.util.ManagedProfileHeuristic;
@@ -1335,7 +1336,7 @@ public class LauncherModel extends BroadcastReceiver
try {
screenIds.add(sc.getLong(idIndex));
} catch (Exception e) {
- addDumpLog("Invalid screen id: " + e);
+ FileLog.d(TAG, "Invalid screen id", e);
}
}
} finally {
@@ -1813,7 +1814,7 @@ public class LauncherModel extends BroadcastReceiver
if (intent == null) {
// The app is installed but the component is no
// longer available.
- addDumpLog("Invalid component removed: " + cn);
+ FileLog.d(TAG, "Invalid component removed: " + cn);
itemsToRemove.add(id);
continue;
} else {
@@ -1824,7 +1825,7 @@ public class LauncherModel extends BroadcastReceiver
} else if (restored) {
// Package is not yet available but might be
// installed later.
- addDumpLog("package not yet restored: " + cn);
+ FileLog.d(TAG, "package not yet restored: " + cn);
if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) {
// Restore has started once.
@@ -1850,12 +1851,12 @@ public class LauncherModel extends BroadcastReceiver
itemReplaced = true;
} else if (REMOVE_UNRESTORED_ICONS) {
- addDumpLog("Unrestored package removed: " + cn);
+ FileLog.d(TAG, "Unrestored package removed: " + cn);
itemsToRemove.add(id);
continue;
}
} else if (REMOVE_UNRESTORED_ICONS) {
- addDumpLog("Unrestored package removed: " + cn);
+ FileLog.d(TAG, "Unrestored package removed: " + cn);
itemsToRemove.add(id);
continue;
}
@@ -1880,7 +1881,7 @@ public class LauncherModel extends BroadcastReceiver
} else {
// Do not wait for external media load anymore.
// Log the invalid package, and remove it
- addDumpLog("Invalid package removed: " + cn);
+ FileLog.d(TAG, "Invalid package removed: " + cn);
itemsToRemove.add(id);
continue;
}
@@ -1890,7 +1891,7 @@ public class LauncherModel extends BroadcastReceiver
restored = false;
}
} catch (URISyntaxException e) {
- addDumpLog("Invalid uri: " + intentDescription);
+ FileLog.d(TAG, "Invalid uri: " + intentDescription);
itemsToRemove.add(id);
continue;
}
@@ -2073,7 +2074,7 @@ public class LauncherModel extends BroadcastReceiver
final boolean isProviderReady = isValidProvider(provider);
if (!isSafeMode && !customWidget &&
wasProviderReady && !isProviderReady) {
- addDumpLog("Deleting widget that isn't installed anymore: "
+ FileLog.d(TAG, "Deleting widget that isn't installed anymore: "
+ provider);
itemsToRemove.add(id);
} else {
@@ -2115,7 +2116,7 @@ public class LauncherModel extends BroadcastReceiver
appWidgetInfo.restoreStatus |=
LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
} else if (REMOVE_UNRESTORED_ICONS && !isSafeMode) {
- addDumpLog("Unrestored widget removed: " + component);
+ FileLog.d(TAG, "Unrestored widget removed: " + component);
itemsToRemove.add(id);
continue;
}
@@ -2171,9 +2172,7 @@ public class LauncherModel extends BroadcastReceiver
}
}
} finally {
- if (c != null) {
- c.close();
- }
+ Utilities.closeSilently(c);
}
// Break early if we've stopped loading
@@ -3541,8 +3540,4 @@ public class LauncherModel extends BroadcastReceiver
public static Looper getWorkerLooper() {
return sWorkerThread.getLooper();
}
-
- @Thunk static final void addDumpLog(String log) {
- Launcher.addDumpLog(TAG, log);
- }
}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 53026ac5a..f10099e0c 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -42,20 +42,25 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
import android.os.Process;
import android.os.UserManager;
+import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherSettings.WorkspaceScreens;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import com.android.launcher3.config.ProviderConfig;
import com.android.launcher3.dynamicui.ExtractionUtils;
import com.android.launcher3.util.ManagedProfileHeuristic;
import com.android.launcher3.util.NoLocaleSqliteContext;
+import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.Thunk;
import java.net.URISyntaxException;
@@ -71,18 +76,19 @@ public class LauncherProvider extends ContentProvider {
public static final String AUTHORITY = ProviderConfig.AUTHORITY;
- static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME;
- static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME;
static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
- private static final Object LISTENER_LOCK = new Object();
- @Thunk LauncherProviderChangeListener mListener;
+ private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
+ private Handler mListenerHandler;
+
protected DatabaseHelper mOpenHelper;
@Override
public boolean onCreate() {
+ mListenerHandler = new Handler(mListenerWrapper);
+
LauncherAppState.setLauncherProvider(this);
return true;
}
@@ -91,9 +97,8 @@ public class LauncherProvider extends ContentProvider {
* Sets a provider listener.
*/
public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
- synchronized (LISTENER_LOCK) {
- mListener = listener;
- }
+ Preconditions.assertUIThread();
+ mListenerWrapper.mListener = listener;
}
@Override
@@ -111,7 +116,7 @@ public class LauncherProvider extends ContentProvider {
*/
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
- mOpenHelper = new DatabaseHelper(getContext(), this);
+ mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
}
}
@@ -159,7 +164,7 @@ public class LauncherProvider extends ContentProvider {
// In very limited cases, we support system|signature permission apps to modify the db.
if (Binder.getCallingPid() != Process.myPid()) {
- if (!mOpenHelper.initializeExternalAdd(initialValues)) {
+ if (!initializeExternalAdd(initialValues)) {
return null;
}
}
@@ -189,6 +194,59 @@ public class LauncherProvider extends ContentProvider {
return uri;
}
+ private boolean initializeExternalAdd(ContentValues values) {
+ // 1. Ensure that externally added items have a valid item id
+ long id = mOpenHelper.generateNewItemId();
+ values.put(LauncherSettings.Favorites._ID, id);
+
+ // 2. In the case of an app widget, and if no app widget id is specified, we
+ // attempt allocate and bind the widget.
+ Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
+ if (itemType != null &&
+ itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
+ !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
+
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
+ ComponentName cn = ComponentName.unflattenFromString(
+ values.getAsString(Favorites.APPWIDGET_PROVIDER));
+
+ if (cn != null) {
+ try {
+ int appWidgetId = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID)
+ .allocateAppWidgetId();
+ values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
+ if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
+ return false;
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to initialize external widget", e);
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ // Add screen id if not present
+ long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
+ SQLiteStatement stmp = null;
+ try {
+ stmp = mOpenHelper.getWritableDatabase().compileStatement(
+ "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " +
+ "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens");
+ stmp.bindLong(1, screenId);
+
+ ContentValues valuesInserted = new ContentValues();
+ valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert());
+ mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted);
+ return true;
+ } catch (Exception e) {
+ return false;
+ } finally {
+ Utilities.closeSilently(stmp);
+ }
+ }
+
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
createDbIfNotExists();
@@ -295,17 +353,7 @@ public class LauncherProvider extends ContentProvider {
.putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors)
.putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId)
.apply();
- new MainThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- synchronized (LISTENER_LOCK) {
- if (mListener != null) {
- mListener.onExtractedColorsChanged();
- }
- }
-
- }
- });
+ mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED);
Bundle result = new Bundle();
result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors);
return result;
@@ -340,6 +388,8 @@ public class LauncherProvider extends ContentProvider {
case LauncherSettings.Settings.METHOD_MIGRATE_LAUNCHER2_SHORTCUTS: {
mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
+ Utilities.getPrefs(getContext()).edit().putBoolean(EMPTY_DATABASE_CREATED, false)
+ .commit();
return null;
}
case LauncherSettings.Settings.METHOD_UPDATE_FOLDER_ITEMS_RANK: {
@@ -373,8 +423,8 @@ public class LauncherProvider extends ContentProvider {
+ LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
+ LauncherSettings.Favorites._ID + " NOT IN (SELECT " +
LauncherSettings.Favorites.CONTAINER + " FROM "
- + TABLE_FAVORITES + ")";
- Cursor c = db.query(TABLE_FAVORITES,
+ + Favorites.TABLE_NAME + ")";
+ Cursor c = db.query(Favorites.TABLE_NAME,
new String[] {LauncherSettings.Favorites._ID},
selection, null, null, null, null);
while (c.moveToNext()) {
@@ -382,7 +432,7 @@ public class LauncherProvider extends ContentProvider {
}
c.close();
if (!folderIds.isEmpty()) {
- db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery(
+ db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
LauncherSettings.Favorites._ID, folderIds), null);
}
db.setTransactionSuccessful();
@@ -401,11 +451,7 @@ public class LauncherProvider extends ContentProvider {
protected void notifyListeners() {
// always notify the backup agent
LauncherBackupAgentHelper.dataChanged(getContext());
- synchronized (LISTENER_LOCK) {
- if (mListener != null) {
- mListener.onLauncherProviderChange();
- }
- }
+ mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
}
@Thunk static void addModifiedTime(ContentValues values) {
@@ -436,10 +482,10 @@ public class LauncherProvider extends ContentProvider {
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
- AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction();
+ AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
+ AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
- loader = AutoInstallsLayout.get(getContext(),
- mOpenHelper.mAppWidgetHost, mOpenHelper);
+ loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(getContext().getPackageManager());
@@ -448,7 +494,7 @@ public class LauncherProvider extends ContentProvider {
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
- loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
+ loader = new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
@@ -456,7 +502,7 @@ public class LauncherProvider extends ContentProvider {
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
- loader = getDefaultLayoutParser();
+ loader = getDefaultLayoutParser(widgetHost);
}
// There might be some partially restored DB items, due to buggy restore logic in
@@ -468,7 +514,7 @@ public class LauncherProvider extends ContentProvider {
// Unable to load external layout. Cleanup and load the internal layout.
createEmptyDB();
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
- getDefaultLayoutParser());
+ getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
@@ -480,7 +526,7 @@ public class LauncherProvider extends ContentProvider {
* @return the loader if the restrictions are set and the resource exists; null otherwise.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
- private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() {
+ private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
// UserManager.getApplicationRestrictions() requires minSdkVersion >= 18
if (!Utilities.ATLEAST_JB_MR2) {
return null;
@@ -499,7 +545,7 @@ public class LauncherProvider extends ContentProvider {
Resources targetResources = ctx.getPackageManager()
.getResourcesForApplication(packageName);
return AutoInstallsLayout.get(ctx, packageName, targetResources,
- mOpenHelper.mAppWidgetHost, mOpenHelper);
+ widgetHost, mOpenHelper);
} catch (NameNotFoundException e) {
Log.e(TAG, "Target package for restricted profile not found", e);
return null;
@@ -508,50 +554,28 @@ public class LauncherProvider extends ContentProvider {
return null;
}
- private DefaultLayoutParser getDefaultLayoutParser() {
+ private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
int defaultLayout = LauncherAppState.getInstance()
.getInvariantDeviceProfile().defaultLayoutId;
- return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
+ return new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, getContext().getResources(), defaultLayout);
}
/**
- * Send notification that we've deleted the {@link AppWidgetHost},
- * probably as part of the initial database creation. The receiver may
- * want to re-call {@link AppWidgetHost#startListening()} to ensure
- * callbacks are correctly set.
- */
- @Thunk void notifyAppHostReset() {
- new MainThreadExecutor().execute(new Runnable() {
-
- @Override
- public void run() {
- synchronized (LISTENER_LOCK) {
- if (mListener != null) {
- mListener.onAppWidgetHostReset();
- }
- }
- }
- });
- }
-
- /**
* The class is subclassed in tests to create an in-memory db.
*/
- protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
- private final LauncherProvider mProvider;
+ public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
+ private final Handler mWidgetHostResetHandler;
private final Context mContext;
- @Thunk final AppWidgetHost mAppWidgetHost;
private long mMaxItemId = -1;
private long mMaxScreenId = -1;
- DatabaseHelper(Context context, LauncherProvider provider) {
- this(context, provider, LauncherFiles.LAUNCHER_DB,
- new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID));
+ DatabaseHelper(Context context, Handler widgetHostResetHandler) {
+ this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
// Table creation sometimes fails silently, which leads to a crash loop.
// This way, we will try to create a table every time after crash, so the device
// would eventually be able to recover.
- if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) {
+ if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
// This operation is a no-op if the table already exists.
addFavoritesTable(getWritableDatabase(), true);
@@ -562,14 +586,13 @@ public class LauncherProvider extends ContentProvider {
}
/**
- * Constructor used only in tests.
+ * Constructor used in tests and for restore.
*/
public DatabaseHelper(
- Context context, LauncherProvider provider, String tableName, AppWidgetHost host) {
+ Context context, Handler widgetHostResetHandler, String tableName) {
super(new NoLocaleSqliteContext(context), tableName, null, DATABASE_VERSION);
mContext = context;
- mProvider = provider;
- mAppWidgetHost = host;
+ mWidgetHostResetHandler = widgetHostResetHandler;
}
protected void initIds() {
@@ -605,12 +628,6 @@ public class LauncherProvider extends ContentProvider {
addFavoritesTable(db, false);
addWorkspacesTable(db, false);
- // Database was just created, so wipe any previous widgets
- if (mAppWidgetHost != null) {
- mAppWidgetHost.deleteHost();
- mProvider.notifyAppHostReset();
- }
-
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
onEmptyDbCreated();
@@ -620,6 +637,13 @@ public class LauncherProvider extends ContentProvider {
* Overriden in tests.
*/
protected void onEmptyDbCreated() {
+ // Database was just created, so wipe any previous widgets
+ if (mWidgetHostResetHandler != null) {
+ new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID).deleteHost();
+ mWidgetHostResetHandler.sendEmptyMessage(
+ ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
+ }
+
// Set the flag for empty DB
Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
@@ -634,35 +658,12 @@ public class LauncherProvider extends ContentProvider {
}
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
- String ifNotExists = optional ? " IF NOT EXISTS " : "";
- db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" +
- "_id INTEGER PRIMARY KEY," +
- "title TEXT," +
- "intent TEXT," +
- "container INTEGER," +
- "screen INTEGER," +
- "cellX INTEGER," +
- "cellY INTEGER," +
- "spanX INTEGER," +
- "spanY INTEGER," +
- "itemType INTEGER," +
- "appWidgetId INTEGER NOT NULL DEFAULT -1," +
- "iconType INTEGER," +
- "iconPackage TEXT," +
- "iconResource TEXT," +
- "icon BLOB," +
- "appWidgetProvider TEXT," +
- "modified INTEGER NOT NULL DEFAULT 0," +
- "restored INTEGER NOT NULL DEFAULT 0," +
- "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," +
- "rank INTEGER NOT NULL DEFAULT 0," +
- "options INTEGER NOT NULL DEFAULT 0" +
- ");");
+ Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
}
private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
- db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" +
+ db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
@@ -673,10 +674,10 @@ public class LauncherProvider extends ContentProvider {
// Delete items directly on the workspace who's screen id doesn't exist
// "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
// AND container = -100"
- String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
+ String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME +
" WHERE " +
LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
- LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
+ LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" +
" AND " +
LauncherSettings.Favorites.CONTAINER + " = " +
LauncherSettings.Favorites.CONTAINER_DESKTOP;
@@ -685,7 +686,7 @@ public class LauncherProvider extends ContentProvider {
// Delete items contained in folders which no longer exist (after above statement)
// "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container
// NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
- String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
+ String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME +
" WHERE " +
LauncherSettings.Favorites.CONTAINER + " <> " +
LauncherSettings.Favorites.CONTAINER_DESKTOP +
@@ -694,16 +695,12 @@ public class LauncherProvider extends ContentProvider {
LauncherSettings.Favorites.CONTAINER_HOTSEAT +
" AND "
+ LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
- LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
+ LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME +
" WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
db.execSQL(removeOrphanedFolderItems);
}
- private void setFlagJustLoadedOldDb() {
- Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
- }
-
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
@@ -823,8 +820,8 @@ public class LauncherProvider extends ContentProvider {
* Clears all the data for a fresh start.
*/
public void createEmptyDB(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
+ db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
onCreate(db);
}
@@ -839,9 +836,8 @@ public class LauncherProvider extends ContentProvider {
try {
// Only consider the primary user as other users can't have a shortcut.
- long userSerial = UserManagerCompat.getInstance(mContext)
- .getSerialNumberForUser(UserHandleCompat.myUserHandle());
- c = db.query(TABLE_FAVORITES, new String[] {
+ long userSerial = getDefaultUserSerial();
+ c = db.query(Favorites.TABLE_NAME, new String[] {
Favorites._ID,
Favorites.INTENT,
}, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
@@ -891,7 +887,7 @@ public class LauncherProvider extends ContentProvider {
public boolean recreateWorkspaceTable(SQLiteDatabase db) {
db.beginTransaction();
try {
- Cursor c = db.query(TABLE_WORKSPACE_SCREENS,
+ Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
new String[] {LauncherSettings.WorkspaceScreens._ID},
null, null, null, null,
LauncherSettings.WorkspaceScreens.SCREEN_RANK);
@@ -909,7 +905,7 @@ public class LauncherProvider extends ContentProvider {
c.close();
}
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
+ db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
addWorkspacesTable(db, false);
// Add all screen ids back
@@ -919,7 +915,7 @@ public class LauncherProvider extends ContentProvider {
values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
addModifiedTime(values);
- db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values);
+ db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
mMaxScreenId = maxId;
@@ -966,12 +962,7 @@ public class LauncherProvider extends ContentProvider {
}
private boolean addProfileColumn(SQLiteDatabase db) {
- UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
- // Default to the serial number of this user, for older
- // shortcuts.
- long userSerialNumber = userManager.getSerialNumberForUser(
- UserHandleCompat.myUserHandle());
- return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber);
+ return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial());
}
private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
@@ -1005,12 +996,12 @@ public class LauncherProvider extends ContentProvider {
@Override
public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
- return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
+ return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
}
public void checkId(String table, ContentValues values) {
long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
- if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
+ if (table == WorkspaceScreens.TABLE_NAME) {
mMaxScreenId = Math.max(id, mMaxScreenId);
} else {
mMaxItemId = Math.max(id, mMaxItemId);
@@ -1018,7 +1009,7 @@ public class LauncherProvider extends ContentProvider {
}
private long initializeMaxItemId(SQLiteDatabase db) {
- return getMaxId(db, TABLE_FAVORITES);
+ return getMaxId(db, Favorites.TABLE_NAME);
}
// Generates a new ID to use for an workspace screen in your database. This method
@@ -1035,94 +1026,7 @@ public class LauncherProvider extends ContentProvider {
}
private long initializeMaxScreenId(SQLiteDatabase db) {
- return getMaxId(db, TABLE_WORKSPACE_SCREENS);
- }
-
- @Thunk boolean initializeExternalAdd(ContentValues values) {
- // 1. Ensure that externally added items have a valid item id
- long id = generateNewItemId();
- values.put(LauncherSettings.Favorites._ID, id);
-
- // 2. In the case of an app widget, and if no app widget id is specified, we
- // attempt allocate and bind the widget.
- Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
- if (itemType != null &&
- itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
- !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
-
- final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
- ComponentName cn = ComponentName.unflattenFromString(
- values.getAsString(Favorites.APPWIDGET_PROVIDER));
-
- if (cn != null) {
- try {
- int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
- values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
- if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
- return false;
- }
- } catch (RuntimeException e) {
- Log.e(TAG, "Failed to initialize external widget", e);
- return false;
- }
- } else {
- return false;
- }
- }
-
- // Add screen id if not present
- long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
- if (!addScreenIdIfNecessary(screenId)) {
- return false;
- }
- return true;
- }
-
- // Returns true of screen id exists, or if successfully added
- private boolean addScreenIdIfNecessary(long screenId) {
- if (!hasScreenId(screenId)) {
- int rank = getMaxScreenRank() + 1;
-
- ContentValues v = new ContentValues();
- v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
- v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
- if (dbInsertAndCheck(this, getWritableDatabase(),
- TABLE_WORKSPACE_SCREENS, null, v) < 0) {
- return false;
- }
- }
- return true;
- }
-
- private boolean hasScreenId(long screenId) {
- SQLiteDatabase db = getWritableDatabase();
- Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
- + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
- if (c != null) {
- int count = c.getCount();
- c.close();
- return count > 0;
- } else {
- return false;
- }
- }
-
- private int getMaxScreenRank() {
- SQLiteDatabase db = getWritableDatabase();
- Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
- + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
-
- // get the result
- final int maxRankIndex = 0;
- int rank = -1;
- if (c != null && c.moveToNext()) {
- rank = c.getInt(maxRankIndex);
- }
- if (c != null) {
- c.close();
- }
-
- return rank;
+ return getMaxId(db, WorkspaceScreens.TABLE_NAME);
}
@Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
@@ -1138,7 +1042,7 @@ public class LauncherProvider extends ContentProvider {
values.clear();
values.put(LauncherSettings.WorkspaceScreens._ID, id);
values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
- if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
+ if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) {
throw new RuntimeException("Failed initialize screen table"
+ "from default layout");
}
@@ -1383,7 +1287,7 @@ public class LauncherProvider extends ContentProvider {
try {
for (ContentValues row: allItems) {
if (row == null) continue;
- if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
+ if (dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, row)
< 0) {
return;
} else {
@@ -1402,7 +1306,7 @@ public class LauncherProvider extends ContentProvider {
final ContentValues values = new ContentValues();
values.put(LauncherSettings.WorkspaceScreens._ID, i);
values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
- if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
+ if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values)
< 0) {
return;
}
@@ -1422,9 +1326,6 @@ public class LauncherProvider extends ContentProvider {
Log.d(TAG, "migrated " + count + " icons from Launcher2 into "
+ (curScreen+1) + " screens");
- // ensure that new screens are created to hold these icons
- setFlagJustLoadedOldDb();
-
// Update max IDs; very important since we just grabbed IDs from another database
mMaxItemId = initializeMaxItemId(db);
mMaxScreenId = initializeMaxScreenId(db);
@@ -1484,4 +1385,31 @@ public class LauncherProvider extends ContentProvider {
}
}
}
+
+ private static class ChangeListenerWrapper implements Handler.Callback {
+
+ private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
+ private static final int MSG_EXTRACTED_COLORS_CHANGED = 2;
+ private static final int MSG_APP_WIDGET_HOST_RESET = 3;
+
+ private LauncherProviderChangeListener mListener;
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (mListener != null) {
+ switch (msg.what) {
+ case MSG_LAUNCHER_PROVIDER_CHANGED:
+ mListener.onLauncherProviderChange();
+ break;
+ case MSG_EXTRACTED_COLORS_CHANGED:
+ mListener.onExtractedColorsChanged();
+ break;
+ case MSG_APP_WIDGET_HOST_RESET:
+ mListener.onAppWidgetHostReset();
+ break;
+ }
+ }
+ return true;
+ }
+ }
}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 0e559a544..13e45476a 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -17,6 +17,7 @@
package com.android.launcher3;
import android.content.ContentResolver;
+import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
@@ -256,6 +257,33 @@ public class LauncherSettings {
* <p>Type: INTEGER</p>
*/
public static final String OPTIONS = "options";
+
+ public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
+ String ifNotExists = optional ? " IF NOT EXISTS " : "";
+ db.execSQL("CREATE TABLE " + ifNotExists + TABLE_NAME + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "title TEXT," +
+ "intent TEXT," +
+ "container INTEGER," +
+ "screen INTEGER," +
+ "cellX INTEGER," +
+ "cellY INTEGER," +
+ "spanX INTEGER," +
+ "spanY INTEGER," +
+ "itemType INTEGER," +
+ "appWidgetId INTEGER NOT NULL DEFAULT -1," +
+ "iconType INTEGER," +
+ "iconPackage TEXT," +
+ "iconResource TEXT," +
+ "icon BLOB," +
+ "appWidgetProvider TEXT," +
+ "modified INTEGER NOT NULL DEFAULT 0," +
+ "restored INTEGER NOT NULL DEFAULT 0," +
+ "profileId INTEGER DEFAULT " + myProfileId + "," +
+ "rank INTEGER NOT NULL DEFAULT 0," +
+ "options INTEGER NOT NULL DEFAULT 0" +
+ ");");
+ }
}
/**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1acbfc12b..c5f601dcd 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -46,7 +46,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PaintDrawable;
import android.os.Build;
-import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.PowerManager;
import android.text.Spannable;
@@ -63,9 +62,11 @@ import android.widget.Toast;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.config.ProviderConfig;
import com.android.launcher3.util.IconNormalizer;
import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -845,6 +846,18 @@ public final class Utilities {
return true;
}
+ public static void closeSilently(Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (IOException e) {
+ if (ProviderConfig.IS_DOGFOOD_BUILD) {
+ Log.d(TAG, "Error closing", e);
+ }
+ }
+ }
+ }
+
/**
* An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
* This allows the badging to be done based on the action bitmap size rather than
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5711a3db7..34c6663e9 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -18,27 +18,15 @@ package com.android.launcher3.config;
/**
* Defines a set of flags used to control various launcher behaviors
- * All the flags must be defined as
- * public static boolean LAUNCHER3_FLAG_NAME = true/false;
- *
- * Use LAUNCHER3_ prefix for prevent namespace conflicts.
*/
public final class FeatureFlags {
private FeatureFlags() {}
- public static boolean IS_DEV_BUILD = false;
- public static boolean IS_ALPHA_BUILD = false;
- public static boolean IS_RELEASE_BUILD = true;
-
// Custom flags go below this
public static boolean LAUNCHER3_DISABLE_ICON_NORMALIZATION = false;
// As opposed to the new spring-loaded workspace.
public static boolean LAUNCHER3_LEGACY_WORKSPACE_DND = false;
public static boolean LAUNCHER3_LEGACY_FOLDER_ICON = false;
- public static boolean LAUNCHER3_LEGACY_LOGGING = false;
public static boolean LAUNCHER3_USE_SYSTEM_DRAG_DRIVER = false;
public static boolean LAUNCHER3_DISABLE_PINCH_TO_OVERVIEW = false;
-
- // This flags is only defined to resolve some build issues.
- public static boolean LAUNCHER3_ICON_NORMALIZATION = false;
}
diff --git a/src/com/android/launcher3/config/ProviderConfig.java b/src/com/android/launcher3/config/ProviderConfig.java
index 825b43422..1d964b1b2 100644
--- a/src/com/android/launcher3/config/ProviderConfig.java
+++ b/src/com/android/launcher3/config/ProviderConfig.java
@@ -20,5 +20,5 @@ public class ProviderConfig {
public static final String AUTHORITY = "com.android.launcher3.settings".intern();
- public static boolean IS_DOGFOOD_BUILD = false;
+ public static boolean IS_DOGFOOD_BUILD = true;
}
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
index 95a62b957..89594f407 100644
--- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java
+++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
@@ -32,6 +32,9 @@ import com.android.launcher3.LauncherSettings;
*/
public class ColorExtractionService extends IntentService {
+ /** The fraction of the wallpaper to extract colors for use on the hotseat. */
+ private static final float HOTSEAT_FRACTION = 1f / 4;
+
public ColorExtractionService() {
super("ColorExtractionService");
}
@@ -44,10 +47,21 @@ public class ColorExtractionService extends IntentService {
if (wallpaperManager.getWallpaperInfo() != null) {
// We can't extract colors from live wallpapers, so just use the default color always.
extractedColors.updatePalette(null);
+ extractedColors.updateHotseatPalette(null);
} else {
Bitmap wallpaper = ((BitmapDrawable) wallpaperManager.getDrawable()).getBitmap();
Palette palette = Palette.from(wallpaper).generate();
extractedColors.updatePalette(palette);
+ // We extract colors for the hotseat separately,
+ // since it only considers the lower part of the wallpaper.
+ // TODO(twickham): update Palette library to 23.3.1 or higher,
+ // which fixes a bug with using regions (b/28349435).
+ Palette hotseatPalette = Palette.from(wallpaper)
+ .setRegion(0, (int) (wallpaper.getHeight() * (1f - HOTSEAT_FRACTION)),
+ wallpaper.getWidth(), wallpaper.getHeight())
+ .clearFilters()
+ .generate();
+ extractedColors.updateHotseatPalette(hotseatPalette);
}
// Save the extracted colors and wallpaper id to LauncherProvider.
diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java
index 4d17ff764..e545288a0 100644
--- a/src/com/android/launcher3/dynamicui/ExtractedColors.java
+++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java
@@ -18,6 +18,7 @@ package com.android.launcher3.dynamicui;
import android.content.Context;
import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
import android.support.v7.graphics.Palette;
import android.util.Log;
@@ -35,26 +36,30 @@ public class ExtractedColors {
// These color profile indices should NOT be changed, since they are used when saving and
// loading extracted colors. New colors should always be added at the end.
- public static final int DEFAULT_INDEX = 0;
- public static final int VIBRANT_INDEX = 1;
- public static final int VIBRANT_DARK_INDEX = 2;
- public static final int VIBRANT_LIGHT_INDEX = 3;
- public static final int MUTED_INDEX = 4;
- public static final int MUTED_DARK_INDEX = 5;
- public static final int MUTED_LIGHT_INDEX = 6;
-
- public static final int NUM_COLOR_PROFILES = 7;
+ public static final int VERSION_INDEX = 0;
+ public static final int HOTSEAT_INDEX = 1;
+ // public static final int VIBRANT_INDEX = 2;
+ // public static final int VIBRANT_DARK_INDEX = 3;
+ // public static final int VIBRANT_LIGHT_INDEX = 4;
+ // public static final int MUTED_INDEX = 5;
+ // public static final int MUTED_DARK_INDEX = 6;
+ // public static final int MUTED_LIGHT_INDEX = 7;
+
+ public static final int NUM_COLOR_PROFILES = 1;
+ private static final int VERSION = 1;
private static final String COLOR_SEPARATOR = ",";
private int[] mColors;
public ExtractedColors() {
- mColors = new int[NUM_COLOR_PROFILES];
+ // The first entry is reserved for the version number.
+ mColors = new int[NUM_COLOR_PROFILES + 1];
+ mColors[VERSION_INDEX] = VERSION;
}
public void setColorAtIndex(int index, int color) {
- if (index >= 0 && index < mColors.length) {
+ if (index > VERSION_INDEX && index < mColors.length) {
mColors[index] = color;
} else {
Log.e(TAG, "Attempted to set a color at an invalid index " + index);
@@ -89,17 +94,21 @@ public class ExtractedColors {
*/
public void load(Context context) {
String encodedString = Utilities.getPrefs(context).getString(
- ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, DEFAULT_COLOR + "");
+ ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, VERSION + "");
decodeFromString(encodedString);
+
+ if (mColors[VERSION_INDEX] != VERSION) {
+ ExtractionUtils.startColorExtractionService(context);
+ }
}
/** @param index must be one of the index values defined at the top of this class. */
- public int getColor(int index) {
- if (index >= 0 && index < mColors.length) {
+ public int getColor(int index, int defaultColor) {
+ if (index > VERSION_INDEX && index < mColors.length) {
return mColors[index];
}
- return DEFAULT_COLOR;
+ return defaultColor;
}
/**
@@ -112,20 +121,39 @@ public class ExtractedColors {
setColorAtIndex(i, ExtractedColors.DEFAULT_COLOR);
}
} else {
- setColorAtIndex(ExtractedColors.DEFAULT_INDEX,
- ExtractedColors.DEFAULT_COLOR);
- setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
- palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
- setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
- palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
- setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
- palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
- setColorAtIndex(ExtractedColors.MUTED_INDEX,
- palette.getMutedColor(ExtractedColors.DEFAULT_COLOR));
- setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
- palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
- setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
- palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+ // We currently don't use any of the colors defined by the Palette API,
+ // but this is how we would add them if we ever need them.
+
+ // setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
+ // palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
+ // setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
+ // palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
+ // setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
+ // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+ // setColorAtIndex(ExtractedColors.MUTED_INDEX,
+ // palette.getMutedColor(DEFAULT_COLOR));
+ // setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
+ // palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
+ // setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
+ // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+ }
+ }
+
+ /**
+ * The hotseat's color is defined as follows:
+ * - 12% black for super light wallpaper
+ * - 18% white for super dark
+ * - 25% white otherwise
+ */
+ public void updateHotseatPalette(Palette hotseatPalette) {
+ int hotseatColor;
+ if (hotseatPalette != null && ExtractionUtils.isSuperLight(hotseatPalette)) {
+ hotseatColor = ColorUtils.setAlphaComponent(Color.BLACK, (int) (0.12f * 255));
+ } else if (hotseatPalette != null && ExtractionUtils.isSuperDark(hotseatPalette)) {
+ hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.18f * 255));
+ } else {
+ hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.25f * 255));
}
+ setColorAtIndex(HOTSEAT_INDEX, hotseatColor);
}
}
diff --git a/src/com/android/launcher3/dynamicui/ExtractionUtils.java b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
index 0b28ba6f9..6dc0035ee 100644
--- a/src/com/android/launcher3/dynamicui/ExtractionUtils.java
+++ b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
@@ -20,11 +20,15 @@ import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v7.graphics.Palette;
import com.android.launcher3.Utilities;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.List;
/**
* Contains helper fields and methods related to extracting colors from the wallpaper.
@@ -34,6 +38,7 @@ public class ExtractionUtils {
public static final String WALLPAPER_ID_PREFERENCE_KEY = "pref_wallpaperId";
private static final int FLAG_SET_SYSTEM = 1 << 0; // TODO: use WallpaperManager.FLAG_SET_SYSTEM
+ private static final float MIN_CONTRAST_RATIO = 2f;
/**
* Extract colors in the :wallpaper-chooser process, if the wallpaper id has changed.
@@ -46,12 +51,17 @@ public class ExtractionUtils {
@Override
public void run() {
if (hasWallpaperIdChanged(context)) {
- context.startService(new Intent(context, ColorExtractionService.class));
+ startColorExtractionService(context);
}
}
});
}
+ /** Starts the {@link ColorExtractionService} without checking the wallpaper id */
+ public static void startColorExtractionService(Context context) {
+ context.startService(new Intent(context, ColorExtractionService.class));
+ }
+
private static boolean hasWallpaperIdChanged(Context context) {
if (!Utilities.isNycOrAbove()) {
// TODO: update an id in sharedprefs in onWallpaperChanged broadcast, and read it here.
@@ -72,4 +82,36 @@ public class ExtractionUtils {
return -1;
}
}
+
+ public static boolean isSuperLight(Palette p) {
+ return !isLegibleOnWallpaper(Color.WHITE, p.getSwatches());
+ }
+
+ public static boolean isSuperDark(Palette p) {
+ return !isLegibleOnWallpaper(Color.BLACK, p.getSwatches());
+ }
+
+ /**
+ * Given a color, returns true if that color is legible on
+ * the given wallpaper color swatches, else returns false.
+ */
+ private static boolean isLegibleOnWallpaper(int color, List<Palette.Swatch> wallpaperSwatches) {
+ int legiblePopulation = 0;
+ int illegiblePopulation = 0;
+ for (Palette.Swatch swatch : wallpaperSwatches) {
+ if (isLegible(color, swatch.getRgb())) {
+ legiblePopulation += swatch.getPopulation();
+ } else {
+ illegiblePopulation += swatch.getPopulation();
+ }
+ }
+ return legiblePopulation > illegiblePopulation;
+ }
+
+ /** @return Whether the foreground color is legible on the background color. */
+ private static boolean isLegible(int foreground, int background) {
+ background = ColorUtils.setAlphaComponent(background, 255);
+ return ColorUtils.calculateContrast(foreground, background) >= MIN_CONTRAST_RATIO;
+ }
+
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7dc815529..1ebe8fdfb 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -330,8 +330,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
}
public void startEditingFolderName() {
- mFolderName.setHint("");
- mIsEditingName = true;
+ post(new Runnable() {
+ @Override
+ public void run() {
+ mFolderName.setHint("");
+ mIsEditingName = true;
+ }
+ });
}
public void dismissEditingName() {
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
new file mode 100644
index 000000000..68d9b8c92
--- /dev/null
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -0,0 +1,216 @@
+package com.android.launcher3.logging;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.ProviderConfig;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around {@link Log} to allow writing to a file.
+ * This class can safely be called from main thread.
+ *
+ * Note: This should only be used for logging errors which have a persistent effect on user's data,
+ * but whose effect may not be visible immediately.
+ */
+public final class FileLog {
+
+ private static final String FILE_NAME_PREFIX = "log-";
+ private static final DateFormat DATE_FORMAT =
+ DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
+
+ private static final long MAX_LOG_FILE_SIZE = 4 << 20; // 4 mb
+
+ private static Handler sHandler = null;
+ private static File sLogsDirectory = null;
+
+ public static void setDir(File logsDir) {
+ sLogsDirectory = logsDir;
+ }
+
+ public static void d(String tag, String msg, Exception e) {
+ Log.d(tag, msg, e);
+ print(tag, msg, e);
+ }
+
+ public static void d(String tag, String msg) {
+ Log.d(tag, msg);
+ print(tag, msg);
+ }
+
+ public static void e(String tag, String msg, Exception e) {
+ Log.e(tag, msg, e);
+ print(tag, msg, e);
+ }
+
+ public static void e(String tag, String msg) {
+ Log.e(tag, msg);
+ print(tag, msg);
+ }
+
+ public static void print(String tag, String msg) {
+ print(tag, msg, null);
+ }
+
+ public static void print(String tag, String msg, Exception e) {
+ if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+ return;
+ }
+ String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
+ if (e != null) {
+ out += "\n" + Log.getStackTraceString(e);
+ }
+ Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
+ }
+
+ private static Handler getHandler() {
+ synchronized (DATE_FORMAT) {
+ if (sHandler == null) {
+ HandlerThread thread = new HandlerThread("file-logger");
+ thread.start();
+ sHandler = new Handler(thread.getLooper(), new LogWriterCallback());
+ }
+ }
+ return sHandler;
+ }
+
+ /**
+ * Blocks until all the pending logs are written to the disk
+ * @param out if not null, all the persisted logs are copied to the writer.
+ */
+ public static void flushAll(PrintWriter out) throws InterruptedException {
+ if (!ProviderConfig.IS_DOGFOOD_BUILD) {
+ return;
+ }
+ CountDownLatch latch = new CountDownLatch(1);
+ Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
+ Pair.create(out, latch)).sendToTarget();
+
+ latch.await(2, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Writes logs to the file.
+ * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
+ * Logs older than 36 hours are purged.
+ */
+ private static class LogWriterCallback implements Handler.Callback {
+
+ private static final long CLOSE_DELAY = 5000; // 5 seconds
+
+ private static final int MSG_WRITE = 1;
+ private static final int MSG_CLOSE = 2;
+ private static final int MSG_FLUSH = 3;
+
+ private String mCurrentFileName = null;
+ private PrintWriter mCurrentWriter = null;
+
+ private void closeWriter() {
+ Utilities.closeSilently(mCurrentWriter);
+ mCurrentWriter = null;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (sLogsDirectory == null || !ProviderConfig.IS_DOGFOOD_BUILD) {
+ return true;
+ }
+ switch (msg.what) {
+ case MSG_WRITE: {
+ Calendar cal = Calendar.getInstance();
+ // suffix with 0 or 1 based on the day of the year.
+ String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+
+ if (!fileName.equals(mCurrentFileName)) {
+ closeWriter();
+ }
+
+ try {
+ if (mCurrentWriter == null) {
+ mCurrentFileName = fileName;
+
+ boolean append = false;
+ File logFile = new File(sLogsDirectory, fileName);
+ if (logFile.exists()) {
+ Calendar modifiedTime = Calendar.getInstance();
+ modifiedTime.setTimeInMillis(logFile.lastModified());
+
+ // If the file was modified more that 36 hours ago, purge the file.
+ // We use instead of 24 to account for day-365 followed by day-1
+ modifiedTime.add(Calendar.HOUR, 36);
+ append = cal.before(modifiedTime)
+ && logFile.length() < MAX_LOG_FILE_SIZE;
+ }
+ mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
+ }
+
+ mCurrentWriter.println((String) msg.obj);
+ mCurrentWriter.flush();
+
+ // Auto close file stream after some time.
+ sHandler.removeMessages(MSG_CLOSE);
+ sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
+ } catch (Exception e) {
+ Log.e("FileLog", "Error writing logs to file", e);
+ // Close stream, will try reopening during next log
+ closeWriter();
+ }
+ return true;
+ }
+ case MSG_CLOSE: {
+ closeWriter();
+ return true;
+ }
+ case MSG_FLUSH: {
+ closeWriter();
+ Pair<PrintWriter, CountDownLatch> p =
+ (Pair<PrintWriter, CountDownLatch>) msg.obj;
+
+ if (p.first != null) {
+ dumpFile(p.first, FILE_NAME_PREFIX + 0);
+ dumpFile(p.first, FILE_NAME_PREFIX + 1);
+ }
+ p.second.countDown();
+ return true;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static void dumpFile(PrintWriter out, String fileName) {
+ File logFile = new File(sLogsDirectory, fileName);
+ if (logFile.exists()) {
+
+ BufferedReader in = null;
+ try {
+ in = new BufferedReader(new FileReader(logFile));
+ out.println();
+ out.println("--- logfile: " + fileName + " ---");
+ String line;
+ while ((line = in.readLine()) != null) {
+ out.println(line);
+ }
+ } catch (Exception e) {
+ // ignore
+ } finally {
+ Utilities.closeSilently(in);
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/LauncherBackupAgentTest.java b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
new file mode 100644
index 000000000..020a557c7
--- /dev/null
+++ b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
@@ -0,0 +1,74 @@
+package com.android.launcher3;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+/**
+ * Tests for {@link LauncherBackupAgent}
+ */
+@MediumTest
+public class LauncherBackupAgentTest extends AndroidTestCase {
+
+ public void testGetProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
+ assertEquals(23, new LauncherBackupAgent().getDefaultProfileId(db));
+ }
+
+ public void testMigrateProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+ // Add some dummy data
+ for (int i = 0; i < 5; i++) {
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, i);
+ values.put(Favorites.TITLE, "item " + i);
+ db.insert(Favorites.TABLE_NAME, null, values);
+ }
+ // Verify item add
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+
+ new LauncherBackupAgent().migrateProfileId(db, 33);
+
+ // verify data migrated
+ assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+
+ // Verify default value changed
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, 100);
+ values.put(Favorites.TITLE, "item 100");
+ db.insert(Favorites.TABLE_NAME, null, values);
+ assertEquals(6, getCount(db, "select * from favorites where profileId = 33"));
+ }
+
+ private int getCount(SQLiteDatabase db, String sql) {
+ Cursor c = db.rawQuery(sql, null);
+ try {
+ return c.getCount();
+ } finally {
+ c.getCount();
+ }
+ }
+
+ private class MyDatabaseHelper extends DatabaseHelper {
+
+ private final long mProfileId;
+
+ public MyDatabaseHelper(long profileId) {
+ super(getContext(), null, null);
+ mProfileId = profileId;
+ }
+
+ @Override
+ public long getDefaultUserSerial() {
+ return mProfileId;
+ }
+
+ protected void onEmptyDbCreated() { }
+ }
+}
diff --git a/tests/src/com/android/launcher3/logging/FileLogTest.java b/tests/src/com/android/launcher3/logging/FileLogTest.java
new file mode 100644
index 000000000..c24cc3fb7
--- /dev/null
+++ b/tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -0,0 +1,77 @@
+package com.android.launcher3.logging;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Calendar;
+
+/**
+ * Tests for {@link FileLog}
+ */
+@SmallTest
+public class FileLogTest extends AndroidTestCase {
+
+ private File mTempDir;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ int count = 0;
+ do {
+ mTempDir = new File(getContext().getCacheDir(), "log-test-" + (count++));
+ } while(!mTempDir.mkdir());
+
+ FileLog.setDir(mTempDir);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Clear existing logs
+ new File(mTempDir, "log-0").delete();
+ new File(mTempDir, "log-1").delete();
+ mTempDir.delete();
+ super.tearDown();
+ }
+
+ public void testPrintLog() throws Exception {
+ FileLog.print("Testing", "hoolalala");
+ StringWriter writer = new StringWriter();
+ FileLog.flushAll(new PrintWriter(writer));
+ assertTrue(writer.toString().contains("hoolalala"));
+
+ FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+ writer = new StringWriter();
+ FileLog.flushAll(new PrintWriter(writer));
+ assertTrue(writer.toString().contains("abracadabra"));
+ // Exception is also printed
+ assertTrue(writer.toString().contains("cat! cat!"));
+
+ // Old logs still present after flush
+ assertTrue(writer.toString().contains("hoolalala"));
+ }
+
+ public void testOldFileTruncated() throws Exception {
+ FileLog.print("Testing", "hoolalala");
+ StringWriter writer = new StringWriter();
+ FileLog.flushAll(new PrintWriter(writer));
+ assertTrue(writer.toString().contains("hoolalala"));
+
+ Calendar threeDaysAgo = Calendar.getInstance();
+ threeDaysAgo.add(Calendar.HOUR, -72);
+ new File(mTempDir, "log-0").setLastModified(threeDaysAgo.getTimeInMillis());
+ new File(mTempDir, "log-1").setLastModified(threeDaysAgo.getTimeInMillis());
+
+ FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
+ writer = new StringWriter();
+ FileLog.flushAll(new PrintWriter(writer));
+ assertTrue(writer.toString().contains("abracadabra"));
+ // Exception is also printed
+ assertTrue(writer.toString().contains("cat! cat!"));
+
+ // Old logs have been truncated
+ assertFalse(writer.toString().contains("hoolalala"));
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/TestLauncherProvider.java b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
index a11013ffe..bd3e86c8a 100644
--- a/tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ b/tests/src/com/android/launcher3/util/TestLauncherProvider.java
@@ -1,6 +1,7 @@
package com.android.launcher3.util;
import android.content.Context;
+import android.database.sqlite.SQLiteOpenHelper;
import com.android.launcher3.LauncherProvider;
@@ -17,16 +18,21 @@ public class TestLauncherProvider extends LauncherProvider {
@Override
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
- mOpenHelper = new MyDatabaseHelper(getContext(), this);
+ mOpenHelper = new MyDatabaseHelper(getContext());
}
}
+ public SQLiteOpenHelper getHelper() {
+ createDbIfNotExists();
+ return mOpenHelper;
+ }
+
@Override
protected void notifyListeners() { }
private static class MyDatabaseHelper extends DatabaseHelper {
- public MyDatabaseHelper(Context context, LauncherProvider provider) {
- super(context, provider, null, null);
+ public MyDatabaseHelper(Context context) {
+ super(context, null, null);
initIds();
}