summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaj Yengisetty <rajesh@cyngn.com>2015-07-27 14:52:21 -0700
committerRaj Yengisetty <rajesh@cyngn.com>2015-07-27 14:52:21 -0700
commit8847eb7a1ddb0b19c0c79de6d867444a68654874 (patch)
tree26b692977e92d5821165a295e08d8d081d05f18d
parent8d355bd41dcb91c7bb1da5a31755381b49fa663e (diff)
downloadandroid_packages_apps_Trebuchet-8847eb7a1ddb0b19c0c79de6d867444a68654874.tar.gz
android_packages_apps_Trebuchet-8847eb7a1ddb0b19c0c79de6d867444a68654874.tar.bz2
android_packages_apps_Trebuchet-8847eb7a1ddb0b19c0c79de6d867444a68654874.zip
Move CMHome into Trebuchetstaging/cmhome
Change-Id: I781d10319d183cfb6acca5926ebf64920caf570e
-rw-r--r--Android.mk32
-rw-r--r--AndroidManifest.xml1
-rw-r--r--api/Android.mk27
-rw-r--r--api/CleanSpec.mk14
-rw-r--r--api/api.iml11
-rw-r--r--api/proguard.flags8
-rw-r--r--api/src/com/android/launcher/home/Home.java217
-rw-r--r--libs/dashclock-api-r2.0.jarbin0 -> 18865 bytes
-rw-r--r--libs/de-hdodenhof-circleimageview.jarbin0 -> 6709 bytes
-rw-r--r--res/drawable-xxhdpi/persona2.pngbin0 -> 333849 bytes
-rw-r--r--res/layout/basic_dashclock_card_inner.xml13
-rw-r--r--res/layout/calendar_card.xml41
-rw-r--r--res/layout/calendar_event_item.xml70
-rw-r--r--res/layout/card_image_layout.xml88
-rw-r--r--res/layout/card_status_header_inner.xml28
-rw-r--r--res/layout/contact_card.xml62
-rw-r--r--res/layout/dashclock_card_expand_inner_content.xml33
-rw-r--r--res/layout/dashclock_card_inner_content.xml34
-rw-r--r--res/layout/dashclock_icon_thumbnail_layout.xml9
-rw-r--r--res/layout/home_layout.xml57
-rw-r--r--res/layout/list_card_image_layout.xml30
-rw-r--r--res/layout/news_card.xml62
-rw-r--r--res/layout/news_card_first.xml73
-rw-r--r--res/layout/simple_message_card_inner_content.xml27
-rw-r--r--res/layout/status_card_inner_content.xml19
-rw-r--r--res/values/colors.xml22
-rw-r--r--res/values/dimens.xml4
-rw-r--r--res/values/strings.xml78
-rw-r--r--res/values/styles.xml23
-rw-r--r--src/com/android/launcher/home/Home.java2
-rw-r--r--src/com/cyanogen/cardbuilder/DataCardFactory.java22
-rw-r--r--src/org/cyanogenmod/launcher/cardprovider/DashClockExtensionCardProvider.java296
-rw-r--r--src/org/cyanogenmod/launcher/cards/DashClockExtensionCard.java446
-rw-r--r--src/org/cyanogenmod/launcher/cards/SimpleMessageCard.java55
-rw-r--r--src/org/cyanogenmod/launcher/cards/StatusCard.java52
-rw-r--r--src/org/cyanogenmod/launcher/cards/StatusCardHeader.java53
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java479
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java390
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java76
-rw-r--r--src/org/cyanogenmod/launcher/home/CMHomeAdapter.java252
-rw-r--r--src/org/cyanogenmod/launcher/home/CMHomeCalendar.java26
-rw-r--r--src/org/cyanogenmod/launcher/home/CMHomeCard.java7
-rw-r--r--src/org/cyanogenmod/launcher/home/CMHomeContact.java22
-rw-r--r--src/org/cyanogenmod/launcher/home/CMHomeNews.java29
-rw-r--r--src/org/cyanogenmod/launcher/home/HomeLauncher.java61
-rw-r--r--src/org/cyanogenmod/launcher/home/HomeLayout.java41
-rw-r--r--src/org/cyanogenmod/launcher/home/HomeStub.java517
-rw-r--r--src/org/cyanogenmod/trebuchet/CustomHomeLauncher.java53
-rw-r--r--src/org/cyanogenmod/trebuchet/home/HomeWrapper.java5
49 files changed, 3948 insertions, 19 deletions
diff --git a/Android.mk b/Android.mk
index b4f2e69e1..3fdb07b5d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -25,15 +25,36 @@ LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 \
android-support-v7-recyclerview \
- guava
+ guava \
+ org.cyanogenmod.launcher.home \
+ CMHomeSDK \
+ dashclockapiv2 \
+ android-support-v13 \
+ cmfm-android-support-v7-appcompat \
+ cmfm-android-support-design \
+ android-support-v7-recyclerview \
+ android-support-v7-cardview \
+ de-hdodenhof-circleimageview
+
+library_src_files += ../../../external/cardslib/library/src/main/java \
+ ../../../external/cyanogen/cmhomeapi/src/main/java
+
LOCAL_SRC_FILES := $(call all-java-files-under, src) \
$(call all-java-files-under, WallpaperPicker/src) \
+ $(call all-java-files-under, $(library_src_files)) \
$(call all-renderscript-files-under, src) \
$(call all-proto-files-under, protos)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/WallpaperPicker/res $(LOCAL_PATH)/res
-LOCAL_AAPT_FLAGS := --auto-add-overlay
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res \
+ $(LOCAL_PATH)/WallpaperPicker/res \
+ frameworks/support/v7/cardview/res \
+ $(LOCAL_PATH)/../../../external/cardslib/library/src/main/res \
+ $(LOCAL_PATH)/../../../external/cyanogen/cmhomeapi/src/main/res
+
+LOCAL_AAPT_FLAGS := --auto-add-overlay \
+ --extra-packages android.support.v7.cardview \
+ --extra-packages it.gmariotti.cardslib.library \
LOCAL_PROTOC_OPTIMIZE_TYPE := nano
LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
@@ -51,6 +72,10 @@ LOCAL_OVERRIDES_PACKAGES := Launcher3
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+ dashclockapiv2:libs/dashclock-api-r2.0.jar \
+ de-hdodenhof-circleimageview:libs/de-hdodenhof-circleimageview.jar
+
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))
@@ -72,6 +97,7 @@ LOCAL_IS_HOST_MODULE := true
LOCAL_JAR_MANIFEST := util/etc/manifest.txt
LOCAL_STATIC_JAVA_LIBRARIES := host-libprotobuf-java-2.3.0-nano
+
include $(BUILD_HOST_JAVA_LIBRARY)
#
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a6a22b9b8..afd273b8c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -98,6 +98,7 @@
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INJECT_EVENTS" />
+ <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_TASKS"/>
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />
diff --git a/api/Android.mk b/api/Android.mk
new file mode 100644
index 000000000..0e33598d6
--- /dev/null
+++ b/api/Android.mk
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2014 The CyanogenMod Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_MODULE := org.cyanogenmod.launcher.home
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/api/CleanSpec.mk b/api/CleanSpec.mk
new file mode 100644
index 000000000..b27df9a5b
--- /dev/null
+++ b/api/CleanSpec.mk
@@ -0,0 +1,14 @@
+# Copyright (C) 2014 The CyanogenMod Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/api/api.iml b/api/api.iml
new file mode 100644
index 000000000..c90834f2d
--- /dev/null
+++ b/api/api.iml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module> \ No newline at end of file
diff --git a/api/proguard.flags b/api/proguard.flags
new file mode 100644
index 000000000..0552c3f92
--- /dev/null
+++ b/api/proguard.flags
@@ -0,0 +1,8 @@
+# configuration
+-dontobfuscate
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* \ No newline at end of file
diff --git a/api/src/com/android/launcher/home/Home.java b/api/src/com/android/launcher/home/Home.java
new file mode 100644
index 000000000..ae8298040
--- /dev/null
+++ b/api/src/com/android/launcher/home/Home.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher.home;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+/**
+ * The generic contract that should supports a <code>Home</code> app to could
+ * be invoked and registered by an Android launcher.<br/>
+ * <br/>
+ * This interface contains the version 1 of the <code>Home Host App</code> protocol.<br/>
+ * <br/>
+ * <br/>
+ * A <code>Home</code> app should:
+ * <ul>
+ * <li>
+ * should have at least a constructor with no arguments
+ * </li>
+ * <li>
+ * declares inside its manifest a <code>com.android.launcher.home</code> metadata with the
+ * full qualified that contains this interface<br/>
+ * <pre>
+ * &lt;meta-data android:name="com.android.launcher.home" value="org.cyanogenmod.launcher.home.HomeStub"/&gt;
+ * </pre>
+ * </li>
+ * <li>
+ * define the "com.android.launcher.home.permissions.HOME_APP" permission<br/>
+ * <pre>
+ * &lt;uses-permission android:name="com.android.launcher.home.permissions.HOME_APP"/&gt;
+ * </pre>
+ * </li>
+ * <li>
+ * implements the contract defined by this protocol.
+ * </li>
+ * </ul>
+ * <br/>
+ * Implementors classes of this protocol should be aware that all the {@link Context} references
+ * passed to this class owns to the host launcher app. This means that you cannot access
+ * to settings defined by the <code>Home</code> app inside its shared context.
+ */
+public interface Home {
+
+ /**
+ * A SHA-1 hash of all declared method of this interface. Home apps should compute as:<br/>
+ * <br/>
+ * <pre>
+ * for method in Home.class.getDeclaredMehod
+ * sha1.update method.toString.bytes
+ * </pre><br/>
+ * DO NOT MODIFY!
+ */
+ public static final String SIGNATURE = "sZFp8JclUBYdIw0QaJZDosZ8SWM=";
+
+ /**
+ * Defines the name of the metadata used to declared the full qualified Home stub class
+ * that implements this protocol.
+ */
+ public static final String METADATA_HOME_STUB = "com.android.launcher.home";
+
+ /**
+ * Defines the name of the permission that the Home app should explicitly declare.
+ */
+ public static final String PERMISSION_HOME_APP = "com.android.launcher.home.permissions.HOME_APP";
+
+ // Notification flags
+ public static final int FLAG_NOTIFY_MASK = 0x0000;
+ public static final int FLAG_NOTIFY_ON_RESUME = FLAG_NOTIFY_MASK + 0x0002;
+ public static final int FLAG_NOTIFY_ON_PAUSE = FLAG_NOTIFY_MASK + 0x0004;
+ public static final int FLAG_NOTIFY_ON_SHOW = FLAG_NOTIFY_MASK + 0x0008;
+ public static final int FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED = FLAG_NOTIFY_MASK + 0x0010;
+ public static final int FLAG_NOTIFY_ON_HIDE = FLAG_NOTIFY_MASK + 0x0020;
+ public static final int FLAG_NOTIFY_ALL = FLAG_NOTIFY_ON_RESUME | FLAG_NOTIFY_ON_PAUSE |
+ FLAG_NOTIFY_ON_SHOW | FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED | FLAG_NOTIFY_ON_HIDE;
+
+ // Operation support flags
+ public static final int FLAG_OP_MASK = 0x1000;
+ public static final int FLAG_OP_CUSTOM_SEARCH = FLAG_OP_MASK + 0x0002;
+ public static final int FLAG_OP_ALL = FLAG_OP_CUSTOM_SEARCH;
+
+ // Search modes
+ public static final int MODE_SEARCH_TEXT = 0x0000;
+ public static final int MODE_SEARCH_VOICE = 0x0001;
+
+ /**
+ * Invoked when creating the Home object to set
+ * a reference to the host Activity that will
+ * contain this instance.
+ * @param context The Activity Context of the host activity.
+ */
+ void setHostActivityContext(Context context);
+
+ /**
+ * Invoked the first time the <code>Home</code> app is created.<br/>
+ * This method should be used by implementors classes of this protocol to load the needed
+ * resources.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onStart(Context context);
+
+ /**
+ * Load and show the content of this home app if true,
+ * hide and remove providers if false.
+ * @param showContent Should content be shown
+ *
+ */
+ void setShowContent(Context context, boolean showContent);
+
+ /**
+ * Invoked when the <code>Home</code> app should be destroy.<br/>
+ * This method should be used by implementors classes of this protocol to unload all unneeded
+ * resources.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onDestroy(Context context);
+
+ /**
+ * Invoked when the host launcher enters in resume mode.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onResume(Context context);
+
+ /**
+ * Invoked when the host launcher enters in pause mode.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onPause(Context context);
+
+ /**
+ * Invoked when the custom content page is totally displayed.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onShow(Context context);
+
+ /**
+ * Invoked when the custom content page is scrolled.
+ * @param context the current {@link Context} of the host launcher.
+ * @param progress the current scroll progress.
+ */
+ void onScrollProgressChanged(Context context, float progress);
+
+ /**
+ * Invoked when the custom content page is totally hidden.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onHide(Context context);
+
+ /**
+ * Invoked by the host launcher to request an invalidation of the ui elements and data used by
+ * the <code>Home</code> implementation class.
+ * @param context the current {@link Context} of the host launcher.
+ */
+ void onInvalidate(Context context);
+
+ /**
+ * Invoked when the host launcher request enter in search mode.
+ * @param context the current {@link Context} of the host launcher.
+ * @param mode the requested search mode. Must be one of:
+ * <ul>
+ * <li>{@link #MODE_SEARCH_TEXT}: Textual mode</li>
+ * <li>{@link #MODE_SEARCH_VOICE}: Voice mode</li>
+ * </ul>
+ */
+ void onRequestSearch(Context context, int mode);
+
+ /**
+ * Returns an instance of a {@link View} that holds the custom content to be displayed
+ * by this <code>Home</code> app.
+ * @param context the current {@link Context} of the host launcher.
+ * @return View The custom content view that will be enclosed inside a
+ * <code>com.android.launcher3.Launcher.QSBScroller</code>.<br/>
+ * Be aware the the height layout of the returned should be defined as
+ * {link {@link LayoutParams#WRAP_CONTENT}, so the view could be scrolled inside the
+ * custom content page.
+ */
+ View createCustomView(Context context);
+
+ /**
+ * Returns the name of the Home app (LIMIT: 30 characters).
+ * @param context the current {@link Context} of the host launcher.
+ */
+ String getName(Context context);
+
+ /**
+ * Implementations should return the combination of notification flags that want to listen to.
+ * @see #FLAG_NOTIFY_ON_RESUME
+ * @see #FLAG_NOTIFY_ON_PAUSE
+ * @see #FLAG_NOTIFY_ON_SHOW
+ * @see #FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED
+ * @see #FLAG_NOTIFY_ON_HIDE
+ * @see #FLAG_NOTIFY_ALL
+ */
+ int getNotificationFlags();
+
+ /**
+ * Implementations should return the combination of operation flags that want they want
+ * to support to.
+ * @see #FLAG_OP_CUSTOM_SEARCH
+ * @see #FLAG_OP_ALL
+ */
+ int getOperationFlags();
+}
diff --git a/libs/dashclock-api-r2.0.jar b/libs/dashclock-api-r2.0.jar
new file mode 100644
index 000000000..18f7bf121
--- /dev/null
+++ b/libs/dashclock-api-r2.0.jar
Binary files differ
diff --git a/libs/de-hdodenhof-circleimageview.jar b/libs/de-hdodenhof-circleimageview.jar
new file mode 100644
index 000000000..541037fdb
--- /dev/null
+++ b/libs/de-hdodenhof-circleimageview.jar
Binary files differ
diff --git a/res/drawable-xxhdpi/persona2.png b/res/drawable-xxhdpi/persona2.png
new file mode 100644
index 000000000..4b182d1f6
--- /dev/null
+++ b/res/drawable-xxhdpi/persona2.png
Binary files differ
diff --git a/res/layout/basic_dashclock_card_inner.xml b/res/layout/basic_dashclock_card_inner.xml
new file mode 100644
index 000000000..7b989e4d7
--- /dev/null
+++ b/res/layout/basic_dashclock_card_inner.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/card_main_inner_simple_title"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="10dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</RelativeLayout>
diff --git a/res/layout/calendar_card.xml b/res/layout/calendar_card.xml
new file mode 100644
index 000000000..814cb2c32
--- /dev/null
+++ b/res/layout/calendar_card.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/calendar_card"
+ android:layout_marginLeft="14dp"
+ android:layout_marginRight="14dp"
+ android:layout_marginTop="20dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/calendar_card_title"
+ android:textSize="16sp"
+ android:layout_marginStart="2dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/home_next_text_divider"
+ android:layout_marginBottom="6dp"/>
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/item_card"
+ card_view:cardElevation="2dp"
+ card_view:cardCornerRadius="3dp"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardBackgroundColor="#bb36474f">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/event_container"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/res/layout/calendar_event_item.xml b/res/layout/calendar_event_item.xml
new file mode 100644
index 000000000..403b0767c
--- /dev/null
+++ b/res/layout/calendar_event_item.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="88dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:textSize="24sp"
+ android:fontFamily="sans-serif-light"
+ android:id="@+id/start_time"
+ android:textColor="@color/calendar_text_primary"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:textSize="12sp"
+ android:layout_marginTop="-4dp"
+ android:fontFamily="sans-serif-light"
+ android:id="@+id/end_time"
+ android:textColor="@color/calendar_text_secondary"/>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@color/calendar_text_secondary"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingTop="4dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:textSize="18sp"
+ android:id="@+id/event_title"
+ android:layout_marginTop="1dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/calendar_text_primary"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:textSize="12sp"
+ android:layout_marginTop="2dp"
+ android:id="@+id/event_location"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/calendar_text_secondary"/>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/card_image_layout.xml b/res/layout/card_image_layout.xml
new file mode 100644
index 000000000..c887fb7c6
--- /dev/null
+++ b/res/layout/card_image_layout.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ *******************************************************************************
+ ~ Copyright (c) 2013-2014 Gabriele Mariotti.
+ ~ Modified 2014 for CyanogenMod.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~ *****************************************************************************
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <!-- Card visible layout -->
+ <LinearLayout
+ android:id="@+id/card_main_layout"
+ style="@style/card.main_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+
+ <!-- Compound view for Header Card
+ If you want to customize this element use attr card:card_header_layout_resourceID
+ You can also use your CardHeader subclass-->
+ <it.gmariotti.cardslib.library.view.component.CardHeaderView
+ style="@style/card.header_outer_layout"
+ android:id="@+id/card_header_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:id="@+id/card_thumb_and_content_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <it.gmariotti.cardslib.library.view.component.CardThumbnailView
+ style="@style/card_thumbnail_outer_layout"
+ android:id="@+id/card_thumbnail_layout"
+ card:card_thumbnail_layout_resourceID="@layout/dashclock_icon_thumbnail_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- Main Content View -->
+ <FrameLayout
+ android:id="@+id/card_main_content_layout"
+ style="@style/card.content_outer_layout"
+ android:layout_marginBottom="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <!-- Compound view for Shadow
+ If you want to customize this element use attr card:card_shadow_layout_resourceID -->
+ <it.gmariotti.cardslib.library.view.component.CardShadowView
+ style="@style/card.shadow_outer_layout"
+ android:id="@+id/card_shadow_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <!-- Expand layout. You can customize this element with your CardExpand class -->
+ <FrameLayout
+ android:id="@+id/card_content_expand_layout"
+ style="@style/card.main_contentExpand"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ </FrameLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/card_status_header_inner.xml b/res/layout/card_status_header_inner.xml
new file mode 100644
index 000000000..bec9b0c24
--- /dev/null
+++ b/res/layout/card_status_header_inner.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:padding="5dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ tools:text="CyanogenMod"
+ android:id="@+id/status_card_title"
+ style="@style/simple_message_card_inner_title_text"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:layout_marginBottom="5dp"
+ android:layout_marginRight="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ tools:text="One day ago"
+ android:id="@+id/status_card_date"
+ android:textStyle="italic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/contact_card.xml b/res/layout/contact_card.xml
new file mode 100644
index 000000000..367e79f84
--- /dev/null
+++ b/res/layout/contact_card.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/contact_card"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginTop="20dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/contact_card_title"
+ android:textSize="16sp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/home_next_text_divider"
+ android:layout_marginBottom="12dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <de.hdodenhof.circleimageview.CircleImageView
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:id="@+id/contact_image_one"/>
+
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <de.hdodenhof.circleimageview.CircleImageView
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:id="@+id/contact_image_two"/>
+
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <de.hdodenhof.circleimageview.CircleImageView
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:id="@+id/contact_image_three"/>
+
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <de.hdodenhof.circleimageview.CircleImageView
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:id="@+id/contact_image_four"/>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/dashclock_card_expand_inner_content.xml b/res/layout/dashclock_card_expand_inner_content.xml
new file mode 100644
index 000000000..bccc03b68
--- /dev/null
+++ b/res/layout/dashclock_card_expand_inner_content.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="horizontal"
+ android:layout_marginBottom="10dp"
+ android:layout_marginTop="10dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ style="@style/card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button
+ tools:text="Action"
+ android:layout_width="0px"
+ android:layout_height="50dp"
+ android:id="@+id/dashclock_card_expand_action_button"
+ android:textColor="@color/card_expand_title_color"
+ android:text="@string/dashclock_card_expand_action"
+ android:textSize="20sp"
+ android:layout_weight="1"
+ />
+ <Button
+ tools:text="Settings"
+ android:layout_width="0px"
+ android:layout_height="50dp"
+ android:id="@+id/dashclock_card_expand_settings_button"
+ android:textColor="@color/card_expand_title_color"
+ android:text="@string/dashclock_card_expand_settings"
+ android:textSize="20sp"
+ android:layout_weight="1"
+ />
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/dashclock_card_inner_content.xml b/res/layout/dashclock_card_inner_content.xml
new file mode 100644
index 000000000..9c02ff9cf
--- /dev/null
+++ b/res/layout/dashclock_card_inner_content.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:gravity="top"
+ style="@style/card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ tools:text="Gmail"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/dashclock_card_inner_title_text"
+ style="@style/card.header_simple_title"
+ android:textSize="20sp"
+ />
+
+ <TextView
+ tools:text="42 new messages"
+ android:id="@+id/dashclock_card_inner_status_text"
+ style="@style/dashclock_card_inner_status_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ tools:text="This is an expanded body message for testing purposes."
+ android:id="@+id/dashclock_card_inner_body_text"
+ style="@style/dashclock_card_inner_body_text"
+ android:lines="4"
+ android:ellipsize="end"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/dashclock_icon_thumbnail_layout.xml b/res/layout/dashclock_icon_thumbnail_layout.xml
new file mode 100644
index 000000000..2e52b3b68
--- /dev/null
+++ b/res/layout/dashclock_icon_thumbnail_layout.xml
@@ -0,0 +1,9 @@
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/card_thumbnail_image"
+ android:layout_width="100dp"
+ android:background="@android:color/black"
+ android:layout_height="100dp"
+ android:padding="10dp"
+ android:layout_margin="10dp"
+ style="@style/card_thumbnail_image"/> \ No newline at end of file
diff --git a/res/layout/home_layout.xml b/res/layout/home_layout.xml
new file mode 100644
index 000000000..04f7da143
--- /dev/null
+++ b/res/layout/home_layout.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The CyanogenMod Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<org.cyanogenmod.launcher.home.HomeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:orientation="vertical"
+ android:layout_marginBottom="16dp">
+
+ <!--<ImageView-->
+ <!--android:id="@+id/header"-->
+ <!--android:layout_width="match_parent"-->
+ <!--android:layout_height="150dp"-->
+ <!--android:src="@drawable/bg_header"-->
+ <!--android:scaleType="centerCrop"/>-->
+
+ <!--<FrameLayout-->
+ <!--android:layout_width="match_parent"-->
+ <!--android:layout_height="match_parent">-->
+
+ <!--<it.gmariotti.cardslib.library.view.CardListView-->
+ <!--android:layout_marginTop="10dp"-->
+ <!--android:layout_width="match_parent"-->
+ <!--android:layout_height="match_parent"-->
+ <!--android:id="@+id/cm_home_cards_list"-->
+ <!--card:list_card_layout_resourceID="@layout/list_card_image_layout"/>-->
+
+ <!--<include layout="@layout/list_card_undo_message"-->
+ <!--android:layout_height="45dp"-->
+ <!--android:layout_width="300dp"-->
+ <!--android:layout_gravity="bottom|center_horizontal"-->
+ <!--android:layout_marginBottom="45dp"/>-->
+
+ <!--</FrameLayout>-->
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/main_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"/>
+
+</org.cyanogenmod.launcher.home.HomeLayout> \ No newline at end of file
diff --git a/res/layout/list_card_image_layout.xml b/res/layout/list_card_image_layout.xml
new file mode 100644
index 000000000..c9abbaa44
--- /dev/null
+++ b/res/layout/list_card_image_layout.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+ <!--
+ ~ *******************************************************************************
+ ~ Copyright (c) 2013-2014 Gabriele Mariotti.
+ ~ Modified 2014 for CyanogenMod.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~ *****************************************************************************
+ -->
+
+<it.gmariotti.cardslib.library.view.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/list_cardId"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/list_card.thumbnail"
+ card:card_layout_resourceID="@layout/card_image_layout"
+/>
diff --git a/res/layout/news_card.xml b/res/layout/news_card.xml
new file mode 100644
index 000000000..2ef51a3f7
--- /dev/null
+++ b/res/layout/news_card.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/news_card"
+ android:layout_marginLeft="14dp"
+ android:layout_marginRight="14dp"
+ android:layout_marginTop="4dp">
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/item_card"
+ card_view:cardCornerRadius="3dp"
+ card_view:cardElevation="2dp"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardBackgroundColor="#bb36474f">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="134dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="88dp"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:id="@+id/news_image"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="12dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:textColor="@color/calendar_text_primary"
+ android:fontFamily="sans-serif-light"
+ android:maxLines="4"
+ android:layout_weight="1"
+ android:id="@+id/news_title"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textColor="@color/calendar_text_secondary"
+ android:id="@+id/news_source_time"
+ android:fontFamily="sans-serif-light"
+ android:layout_weight="0"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/res/layout/news_card_first.xml b/res/layout/news_card_first.xml
new file mode 100644
index 000000000..d63039db1
--- /dev/null
+++ b/res/layout/news_card_first.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/news_card"
+ android:layout_marginLeft="14dp"
+ android:layout_marginRight="14dp"
+ android:layout_marginTop="20dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/news_card_title"
+ android:textSize="16sp"
+ android:layout_marginStart="2dp"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/home_next_text_divider"
+ android:layout_marginBottom="12dp"/>
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/item_card"
+ card_view:cardCornerRadius="3dp"
+ card_view:cardElevation="2dp"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardBackgroundColor="#bb36474f">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="134dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:layout_width="88dp"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:id="@+id/news_image"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="12dp">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:textColor="@color/calendar_text_primary"
+ android:fontFamily="sans-serif-light"
+ android:maxLines="4"
+ android:layout_weight="1"
+ android:id="@+id/news_title"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textColor="@color/calendar_text_secondary"
+ android:id="@+id/news_source_time"
+ android:fontFamily="sans-serif-light"
+ android:layout_weight="0"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/res/layout/simple_message_card_inner_content.xml b/res/layout/simple_message_card_inner_content.xml
new file mode 100644
index 000000000..c2b43ed6b
--- /dev/null
+++ b/res/layout/simple_message_card_inner_content.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:gravity="top"
+ style="@style/card"
+ android:layout_margin="15dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ tools:text="Testing Title"
+ android:id="@+id/simple_message_card_title"
+ style="@style/simple_message_card_inner_title_text"
+ android:lines="1"
+ android:ellipsize="end"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ tools:text="This is an example message for testing purposes."
+ android:id="@+id/simple_message_card_text"
+ style="@style/simple_message_card_inner_body_text"
+ android:layout_marginTop="5dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/status_card_inner_content.xml b/res/layout/status_card_inner_content.xml
new file mode 100644
index 000000000..82cdc50b1
--- /dev/null
+++ b/res/layout/status_card_inner_content.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:gravity="top"
+ style="@style/card"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ tools:text="Cards are awesome!!"
+ android:id="@+id/status_card_status_text"
+ style="@style/simple_message_card_inner_body_text"
+ android:layout_marginTop="5dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 901604d70..c6fa69789 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -59,4 +59,26 @@
<color name="aftv_shadowColor">#b0000000</color>
<color name="folder_background">#141a1e</color>
+
+ <!-- CM Home -->
+ <!-- Card Color -->
+ <color name="card_background">#FFF</color>
+ <color name="icon_background_orange">#F9A43E</color>
+ <color name="icon_background_purple">#AD62A7</color>
+ <color name="icon_background_yellow">#E4C62E</color>
+ <color name="icon_background_pink">#F16364</color>
+ <color name="icon_background_blue">#2093CD</color>
+
+ <array name="icon_background_colors">
+ <item>@color/icon_background_orange</item>
+ <item>@color/icon_background_purple</item>
+ <item>@color/icon_background_yellow</item>
+ <item>@color/icon_background_pink</item>
+ <item>@color/icon_background_blue</item>
+ </array>
+
+ <color name="home_next_text_divider">#FFF</color>
+ <color name="calendar_text_primary">#FFF</color>
+ <color name="calendar_text_secondary">#7cffffff</color>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 97a12abdc..d9dde980b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -145,4 +145,8 @@
<!-- Folder open animation -->
<integer name="folder_translate_y_dist">300</integer>
<integer name="folder_icon_translate_y_dist">100</integer>
+
+ <!-- CM Home -->
+ <dimen name="card_background_default_radius">2dip</dimen>
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index adaa25a04..c7e6952da 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -308,4 +308,82 @@ s -->
<string name="abandoned_promise_explanation">The app for this icon isn\'t installed.
You can remove it, or search for the app and install it manually.
</string>
+
+ <!-- CM Home -->
+ <!-- Permissions -->
+ <string name="permission_label_read_extension_data">request DashClock extension data</string>
+ <string name="permission_desc_read_extension_data">Allows the app to request DashClock extension data</string>
+
+ <!-- CM Home API permissions -->
+ <string name="permission_desc_feed_host">Allows the app to read and write to any application
+ that publishes cards to CM Home</string>
+ <string name="permission_label_feed_host">publish and unpublish cards from CM Home</string>
+ <string name="permission_desc_feed_publish">Allows the app to add new cards to CM Home</string>
+ <string name="permission_label_feed_publish">publish cards to launcher CM Home</string>
+
+ <string name="dashclock_activity_not_found_toast_message">The intended Activity for this extension was not found. Please contact the developer for more info.</string>
+ <string name="dashclock_card_expand_action">Action</string>
+ <string name="dashclock_card_expand_settings">Settings</string>
+ <string name="no_extensions_card_title">Get some extensions!</string>
+ <string name="no_extensions_card_body">Looks like you don\'t have any installed extensions. CM Home currently supports DashClock extensions, so go get some and check back here!</string>
+
+ <string name="contact_card_title">Recent Contacts</string>
+ <string name="calendar_card_title">Today</string>
+ <string name="news_card_title">News</string>
+
+
+ <string-array name="contact_card">
+ <item name="uri_1">URI one</item>
+ <item name="uri_2">URI two</item>
+ <item name="uri_3">URI three</item>
+ <item name="uri_4">URI four</item>
+ </string-array>
+
+ <string-array name="calendar_card">
+ <item name="start_time">1437850800000</item>
+ <item name="end_time">1437811200000</item>
+ <item name="title">Stand-up</item>
+ <item name="location">Conference Room</item>
+ </string-array>
+
+ <string-array name="news_1">
+ <item name="news_image_url">http://slidell-independent.com/wp-content/uploads/2013/01/wsne.jpg</item>
+ <item name="news_title">Fiat recalls 1.4 million vehicles after hack of Jeep Cherokee</item>
+ <item name="source">Los Angeles Times</item>
+ <item name="time">1437834180000</item>
+ <item name="url">www.google.com</item>
+ </string-array>
+
+ <string-array name="news_2">
+ <item name="news_image_url">http://slidell-independent.com/wp-content/uploads/2013/01/wsne.jpg</item>
+ <item name="news_title">Fiat recalls 1.4 million vehicles after hack of Jeep Cherokee</item>
+ <item name="source">Los Angeles Times</item>
+ <item name="time">1437834180000</item>
+ <item name="url">www.google.com</item>
+ </string-array>
+
+ <string-array name="news_3">
+ <item name="news_image_url">http://slidell-independent.com/wp-content/uploads/2013/01/wsne.jpg</item>
+ <item name="news_title">Fiat recalls 1.4 million vehicles after hack of Jeep Cherokee</item>
+ <item name="source">Los Angeles Times</item>
+ <item name="time">1437834180000</item>
+ <item name="url">www.google.com</item>
+ </string-array>
+
+ <string-array name="news_4">
+ <item name="news_image_url">http://slidell-independent.com/wp-content/uploads/2013/01/wsne.jpg</item>
+ <item name="news_title">Fiat recalls 1.4 million vehicles after hack of Jeep Cherokee</item>
+ <item name="source">Los Angeles Times</item>
+ <item name="time">1437834180000</item>
+ <item name="url">www.google.com</item>
+ </string-array>
+
+ <string-array name="spoof_data">
+ <item>@array/contact_card</item>
+ <item>@array/calendar_card</item>
+ <item>@array/news_1</item>
+ <item>@array/news_2</item>
+ <item>@array/news_3</item>
+ <item>@array/news_4</item>
+ </string-array>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index bb0401868..02c98b96b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -138,4 +138,27 @@
<style name="SearchButton.WithPaddingStart">
<item name="android:paddingLeft">8dp</item>
</style>
+
+ <!-- CM Home -->
+ <style name="dashclock_card_inner_status_text">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@android:color/darker_gray</item>
+ <item name="android:layout_marginLeft">@dimen/card_header_simple_title_margin_left</item>
+ </style>
+
+ <style name="dashclock_card_inner_body_text">
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_marginLeft">@dimen/card_header_simple_title_margin_left</item>
+ <item name="android:textColor">@android:color/black</item>
+ </style>
+
+ <style name="simple_message_card_inner_title_text">
+ <item name="android:textSize">20sp</item>
+ <item name="android:textColor">@android:color/black</item>
+ </style>
+ <style name="simple_message_card_inner_body_text">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@android:color/black</item>
+ </style>
+
</resources>
diff --git a/src/com/android/launcher/home/Home.java b/src/com/android/launcher/home/Home.java
index e6fedc8db..6524fecb8 100644
--- a/src/com/android/launcher/home/Home.java
+++ b/src/com/android/launcher/home/Home.java
@@ -118,7 +118,7 @@ public interface Home {
* hide and remove providers if false.
* @param showContent Should content be shown
*/
- void setShowContent(boolean showContent);
+ void setShowContent(Context context, boolean showContent);
/**
* Invoked when the <code>Home</code> app should be destroy.<br/>
diff --git a/src/com/cyanogen/cardbuilder/DataCardFactory.java b/src/com/cyanogen/cardbuilder/DataCardFactory.java
new file mode 100644
index 000000000..8a7a4271a
--- /dev/null
+++ b/src/com/cyanogen/cardbuilder/DataCardFactory.java
@@ -0,0 +1,22 @@
+package com.cyanogen.cardbuilder;
+
+import android.content.Context;
+import android.text.TextUtils;
+import it.gmariotti.cardslib.library.internal.Card;
+import org.cyanogenmod.launcher.cards.StatusCard;
+import org.cyanogenmod.launcher.home.api.cards.CardData;
+
+public class DataCardFactory {
+ public static Card createCard(Context context, CardData cardData) {
+ Card card = null;
+ if (cardDataCanDisplayAsStatusCard(cardData)) {
+ card = new StatusCard(context, cardData);
+ }
+ return card;
+ }
+
+ private static boolean cardDataCanDisplayAsStatusCard(CardData cardData) {
+ return !TextUtils.isEmpty(cardData.getTitle()) &&
+ !TextUtils.isEmpty(cardData.getBodyText());
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/cardprovider/DashClockExtensionCardProvider.java b/src/org/cyanogenmod/launcher/cardprovider/DashClockExtensionCardProvider.java
new file mode 100644
index 000000000..afcbdc842
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/cardprovider/DashClockExtensionCardProvider.java
@@ -0,0 +1,296 @@
+package org.cyanogenmod.launcher.cardprovider;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import org.cyanogenmod.launcher.cards.CmCard;
+import org.cyanogenmod.launcher.cards.DashClockExtensionCard;
+import org.cyanogenmod.launcher.dashclock.ExtensionHost;
+import org.cyanogenmod.launcher.dashclock.ExtensionManager;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import it.gmariotti.cardslib.library.internal.Card;
+
+/**
+ * Manages fetching data from all installed DashClock extensions
+ * and generates cards to be displayed.
+ */
+public class DashClockExtensionCardProvider implements ICardProvider, ExtensionManager.OnChangeListener {
+ public static final String TAG = "DashClockExtensionCardProvider";
+ public static final String EXTENSION_TIMEOUT_SHARED_PREF_FILE = "DashClockExtensionTimeouts";
+ public static final int CARD_REAPPEAR_TIME_IN_MINUTES = 180; // three hours
+
+ private ExtensionManager mExtensionManager;
+ private ExtensionHost mExtensionHost;
+ private Context mContext;
+ private Context mHostActivityContext;
+ private List<CardProviderUpdateListener> mUpdateListeners = new ArrayList<CardProviderUpdateListener>();
+
+ public DashClockExtensionCardProvider(Context context, Context hostActivityContext) {
+ mContext = context;
+ mHostActivityContext = hostActivityContext;
+ mExtensionManager = ExtensionManager.getInstance(context, hostActivityContext);
+ mExtensionManager.addOnChangeListener(this);
+ mExtensionHost = new ExtensionHost(context, hostActivityContext);
+
+ trackAllExtensions();
+ }
+
+ @Override
+ public void onShow() {
+ mExtensionHost.init();
+ mExtensionManager.addOnChangeListener(this);
+ trackAllExtensions();
+ }
+
+ @Override
+ public void onDestroy(Context context) {
+ mExtensionManager.removeOnChangeListener(this);
+ mExtensionHost.destroy();
+ mExtensionManager.setActiveExtensions(new ArrayList<ComponentName>());
+ }
+
+ @Override
+ public void onHide(Context context) {
+ // Tear down the extension connections when the app is hidden,
+ // so that we don't block other readers (i.e. actual dashclock).
+ mExtensionManager.removeOnChangeListener(this);
+ mExtensionHost.destroy();
+ mExtensionManager.setActiveExtensions(new ArrayList<ComponentName>());
+ }
+
+ @Override
+ public List<CmCard> getCards() {
+ List<CmCard> cards = new ArrayList<CmCard>();
+
+ for(ExtensionManager.ExtensionWithData extensionWithData :
+ mExtensionManager.getActiveExtensionsWithData()) {
+ if(extensionWithData.latestData != null
+ && extensionWithData.latestData.visible()
+ && shouldReappear(extensionWithData.listing.componentName.flattenToString())
+ && !TextUtils.isEmpty(extensionWithData.latestData.status())) {
+ DashClockExtensionCard card = new DashClockExtensionCard(mContext,
+ extensionWithData,
+ mHostActivityContext);
+ setCardSwipeAndUndoListeners(card);
+ cards.add(card);
+ }
+ }
+
+ return cards;
+ }
+
+ @Override
+ public void requestRefresh() {
+ trackAllExtensions();
+ mExtensionHost.requestAllManualUpdate();
+ }
+
+ @Override
+ public CardProviderUpdateResult updateAndAddCards(List<CmCard> cards) {
+ List<ExtensionManager.ExtensionWithData> extensions
+ = mExtensionManager.getActiveExtensionsWithData();
+
+ // A List of cards to return that must be removed
+ List<CmCard> cardsToRemove = new ArrayList<CmCard>();
+
+ // Create a map from ComponentName String -> extensionWithData
+ HashMap<String, ExtensionManager.ExtensionWithData> map
+ = new HashMap<String, ExtensionManager.ExtensionWithData>();
+ for(ExtensionManager.ExtensionWithData extension : extensions) {
+ map.put(extension.listing.componentName.flattenToString(), extension);
+ }
+
+ for(CmCard card : cards) {
+ if(card instanceof DashClockExtensionCard) {
+ DashClockExtensionCard dashClockExtensionCard
+ = (DashClockExtensionCard) card;
+ if(map.containsKey(dashClockExtensionCard
+ .getFlattenedComponentNameString())) {
+ ExtensionManager.ExtensionWithData extensionWithData
+ = map.get(dashClockExtensionCard
+ .getFlattenedComponentNameString());
+ if (extensionWithData.latestData.visible()
+ && shouldReappear(extensionWithData.
+ listing.componentName.flattenToString())) {
+ dashClockExtensionCard
+ .updateFromExtensionWithData(extensionWithData);
+ } else {
+ cardsToRemove.add(dashClockExtensionCard);
+ }
+ map.remove(dashClockExtensionCard.getFlattenedComponentNameString());
+ }
+ }
+ }
+
+ // A List of cards to return that must be added
+ List<CmCard> cardsToAdd = new ArrayList<CmCard>();
+
+ // Create new cards for extensions that were not represented
+ for(Map.Entry<String, ExtensionManager.ExtensionWithData> entry : map.entrySet()) {
+ ExtensionManager.ExtensionWithData extension = entry.getValue();
+
+ if(extension.latestData != null && !TextUtils.isEmpty(extension.latestData.status())) {
+ DashClockExtensionCard card =
+ new DashClockExtensionCard(mContext, extension, mHostActivityContext);
+ if (extension.latestData.visible()
+ && shouldReappear(extension.listing.componentName.flattenToString())) {
+ setCardSwipeAndUndoListeners(card);
+ cardsToAdd.add(card);
+ }
+ }
+ }
+
+ return new CardProviderUpdateResult(cardsToAdd, cardsToRemove);
+ }
+
+ private void setCardSwipeAndUndoListeners(DashClockExtensionCard card) {
+ card.setOnSwipeListener(new Card.OnSwipeListener() {
+ @Override
+ public void onSwipe(Card card) {
+ storeReappearTime(card.getId(),
+ getReappearTimeFromNow());
+ }
+ });
+
+ card.setOnUndoSwipeListListener(new Card.OnUndoSwipeListListener() {
+ @Override
+ public void onUndoSwipe(Card card, boolean timedOut) {
+ if (!timedOut) {
+ clearReappearTime(card.getId());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void updateCard(CmCard card) {
+ if (!(card instanceof DashClockExtensionCard)) {
+ return;
+ }
+
+ List<ExtensionManager.ExtensionWithData> extensions
+ = mExtensionManager.getActiveExtensionsWithData();
+
+ for(ExtensionManager.ExtensionWithData extension : extensions) {
+ if (extension.listing.componentName.flattenToString()
+ .equals(card.getId())) {
+ ((DashClockExtensionCard) card)
+ .updateFromExtensionWithData(extension);
+ }
+ }
+ }
+
+ public CmCard createCardForId(String id) {
+ List<ExtensionManager.ExtensionWithData> extensions
+ = mExtensionManager.getActiveExtensionsWithData();
+
+ for(ExtensionManager.ExtensionWithData extension : extensions) {
+ if (extension.listing.componentName.flattenToString()
+ .equals(id)
+ && extension.latestData.visible()
+ && shouldReappear(extension.listing.componentName.flattenToString())) {
+ DashClockExtensionCard card =
+ new DashClockExtensionCard(mContext, extension,
+ mHostActivityContext);
+ setCardSwipeAndUndoListeners(card);
+ return card;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void onExtensionsChanged(ComponentName sourceExtension) {
+ if (sourceExtension != null) {
+ for (CardProviderUpdateListener listener : mUpdateListeners) {
+ listener.onCardProviderUpdate(sourceExtension.flattenToString(), false);
+ }
+ }
+ }
+
+ /**
+ * Retrieves a list of all available extensions installed on the device
+ * and sets mExtensionManager to track them for updates.
+ */
+ private void trackAllExtensions() {
+ List<ComponentName> availableComponents = new ArrayList<ComponentName>();
+ for(ExtensionManager.ExtensionListing listing : mExtensionManager.getAvailableExtensions()) {
+ availableComponents.add(listing.componentName);
+ }
+ mExtensionManager.setActiveExtensions(availableComponents);
+ }
+
+ /**
+ * Adds a listener for any extension updates.
+ * @param listener The listener to update
+ */
+ @Override
+ public void addOnUpdateListener(CardProviderUpdateListener listener) {
+ mUpdateListeners.add(listener);
+ }
+
+ /**
+ * Gets the time in the future when a card
+ * should reappear, if it has been dismissed now.
+ * @return The time in millis when the card should be allowed to reappear
+ */
+ private long getReappearTimeFromNow() {
+ Calendar now = Calendar.getInstance();
+ now.add(Calendar.MINUTE, CARD_REAPPEAR_TIME_IN_MINUTES);
+ return now.getTimeInMillis();
+ }
+
+ /**
+ * Gets the stored time at which the card should be allowed to reappear.
+ * @param extensionKey The DashClock extension ComponentName String that will be the key
+ * @return The time in millis at which the card can reappear OR zero if no time is stored.
+ */
+ private long getStoredReappearTime(String extensionKey) {
+ SharedPreferences preferences =
+ mHostActivityContext.getSharedPreferences(EXTENSION_TIMEOUT_SHARED_PREF_FILE,
+ Context.MODE_PRIVATE);
+ return preferences.getLong(extensionKey, 0);
+ }
+
+ private void storeReappearTime(String extensionKey, long returnTime) {
+ SharedPreferences preferences =
+ mHostActivityContext.getSharedPreferences(EXTENSION_TIMEOUT_SHARED_PREF_FILE,
+ Context.MODE_PRIVATE);
+ preferences.edit().putLong(extensionKey, returnTime).apply();
+ }
+
+ private void clearReappearTime(String extensionKey) {
+ SharedPreferences preferences =
+ mHostActivityContext.getSharedPreferences(EXTENSION_TIMEOUT_SHARED_PREF_FILE,
+ Context.MODE_PRIVATE);
+ preferences.edit().remove(extensionKey).apply();
+ }
+
+ /**
+ * Checks if the card representing the extensionKey parameter should be allowed to appear.
+ * @param extensionKey The flattened ComponentName String representing an extension.
+ * @return True if the current time is after the stored reappearance time OR
+ * if there is no stored time for this extension. False if the stored time
+ * is still in the future.
+ */
+ private boolean shouldReappear(String extensionKey) {
+ Calendar now = Calendar.getInstance();
+ long reappearTime = getStoredReappearTime(extensionKey);
+ boolean shouldReappear = true;
+ if (reappearTime != 0) {
+ Calendar reappear = Calendar.getInstance();
+ reappear.setTimeInMillis(reappearTime);
+ shouldReappear = now.after(reappear);
+ }
+ return shouldReappear;
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/cards/DashClockExtensionCard.java b/src/org/cyanogenmod/launcher/cards/DashClockExtensionCard.java
new file mode 100644
index 000000000..c59f2d98d
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/cards/DashClockExtensionCard.java
@@ -0,0 +1,446 @@
+package org.cyanogenmod.launcher.cards;
+
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+import com.google.android.apps.dashclock.api.ExtensionData;
+
+import it.gmariotti.cardslib.library.internal.Card;
+import org.cyanogenmod.launcher.dashclock.ExtensionManager;
+
+import java.io.FileNotFoundException;
+
+import it.gmariotti.cardslib.library.internal.CardExpand;
+import it.gmariotti.cardslib.library.internal.CardHeader;
+import it.gmariotti.cardslib.library.internal.CardThumbnail;
+import it.gmariotti.cardslib.library.internal.ViewToClickToExpand;
+
+/**
+ * This class provides a card that will represent a DashClock Extension
+ */
+public class DashClockExtensionCard extends CmCard {
+ private final static String TAG = "DashClockExtensionCard";
+ private ExtensionManager.ExtensionWithData mExtensionWithData;
+ private Context mHostActivityContext;
+ private String mFlattenedComponentNameString = "";
+
+ public DashClockExtensionCard(Context context,
+ ExtensionManager.ExtensionWithData extensionWithData,
+ Context hostActivityContext) {
+ this(context, extensionWithData,
+ R.layout.dashclock_card_inner_content, hostActivityContext);
+ }
+
+ public DashClockExtensionCard(Context context,
+ ExtensionManager.ExtensionWithData extensionWithData,
+ int innerLayout,
+ Context hostActivityContext) {
+ super(context, innerLayout);
+ mExtensionWithData = extensionWithData;
+ mHostActivityContext = hostActivityContext;
+ init();
+ }
+
+ private void init() {
+ // Track the ComponentName of the extension driving this card
+ mFlattenedComponentNameString
+ = mExtensionWithData.listing.componentName.flattenToString();
+
+ //Add Header
+ CardHeader header = new CardHeader(getContext());
+ header.setTitle(getHeaderTitleFromExtension());
+ addCardHeader(header);
+
+ addCardIcon();
+
+ DashClockCardExpand cardExpand = new DashClockCardExpand(getContext());
+ cardExpand.onExtensionUpdate();
+ addCardExpand(cardExpand);
+
+ setSwipeable(true);
+
+ setId(mFlattenedComponentNameString);
+ }
+
+ @Override
+ public void onUndoSwipe(Card card, boolean timedOut) {
+ // TODO Store the ID of the card that was swiped, so we can not bring it back unless
+ // we want to for some new reason
+ }
+
+ public void updateFromExtensionWithData(ExtensionManager.ExtensionWithData extensionWithData) {
+ if(TextUtils.isEmpty(extensionWithData.latestData.expandedBody())
+ && TextUtils.isEmpty(extensionWithData.latestData.status())
+ && TextUtils.isEmpty(extensionWithData.latestData.expandedTitle())) {
+ // Empty update, don't continue.
+ return;
+ }
+ mExtensionWithData = extensionWithData;
+
+ init();
+ }
+
+ private void addCardIcon() {
+ ExtensionData data = mExtensionWithData.latestData;
+ if(getCardThumbnail() == null
+ && (data.iconUri() != null
+ || data.icon() > 0)) {
+ CardThumbnail thumbnail = new DashClockThumbnail(mContext);
+ thumbnail.setCustomSource(new DashClockIconCardThumbnailSource(mContext, mExtensionWithData.listing.componentName, data));
+ addCardThumbnail(thumbnail);
+ } else if (data.iconUri() != null || data.icon() > 0) {
+ CardThumbnail thumbnail = getCardThumbnail();
+ DashClockIconCardThumbnailSource thumbnailSource =
+ (DashClockIconCardThumbnailSource) thumbnail.getCustomSource();
+ thumbnailSource.setExtensionData(data);
+ boolean shouldUpdate = thumbnailSource.shouldUpdateThumbnail();
+ if (shouldUpdate) {
+ addCardThumbnail(thumbnail);
+ }
+ }
+ }
+
+ public String getFlattenedComponentNameString() {
+ return mFlattenedComponentNameString;
+ }
+
+ private String getHeaderTitleFromExtension() {
+ ExtensionData data = mExtensionWithData.latestData;
+ String title = "";
+
+ if(!TextUtils.isEmpty(mExtensionWithData.listing.title)) {
+ title = mExtensionWithData.listing.title;
+ } else if(!TextUtils.isEmpty(data.expandedTitle())) {
+ title = data.expandedTitle();
+ } else if(!TextUtils.isEmpty(data.status())) {
+ title = data.status();
+ }
+ return title;
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View view) {
+ setupInnerView(view);
+ }
+
+ public void setupInnerView(View view) {
+ TextView titleTextView = (TextView) view.findViewById(R.id.dashclock_card_inner_title_text);
+ TextView statusTextView = (TextView) view.findViewById(R.id.dashclock_card_inner_status_text);
+ TextView bodyTextView = (TextView) view.findViewById(R.id.dashclock_card_inner_body_text);
+
+ String title = mExtensionWithData.latestData.expandedTitle();
+ String status = mExtensionWithData.latestData.status();
+ String body = mExtensionWithData.latestData.expandedBody();
+
+ if(TextUtils.isEmpty(title) && !TextUtils.isEmpty(status)) {
+ titleTextView.setText(status);
+ statusTextView.setVisibility(View.GONE);
+ } else {
+ titleTextView.setText(title);
+ statusTextView.setText(status);
+ statusTextView.setVisibility(View.VISIBLE);
+ }
+
+ bodyTextView.setText(body);
+
+ // Clicking the card expands it, if one of the buttons is enabled
+ DashClockCardExpand cardExpand = (DashClockCardExpand)getCardExpand();
+ if (!cardExpand.getActionEnabled() && !cardExpand.getSettingsEnabled()) {
+ setViewToClickToExpand(null);
+ } else {
+ ViewToClickToExpand viewToClickToExpand =
+ ViewToClickToExpand.builder().setupView(getCardView());
+ setViewToClickToExpand(viewToClickToExpand);
+ }
+ }
+
+ private static class DashClockThumbnail extends CardThumbnail {
+ private static int[] sIconBackgroundColors;
+
+ private static int sCurrentIconColorIndex = 0;
+ private int mIconColorIndex = -1;
+
+ public DashClockThumbnail(Context context) {
+ super(context);
+ sIconBackgroundColors =
+ context.getResources().getIntArray(R.array.icon_background_colors);
+
+ // Assign this card a color, incrementing the static ongoing color index
+ if(mIconColorIndex == -1) {
+ mIconColorIndex = sCurrentIconColorIndex++ % sIconBackgroundColors.length;
+ }
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View viewImage) {
+ ImageView image= (ImageView) viewImage;
+
+ // Pick the next background color for the icon.
+ // Choose the color in the order they appear in ICON_BACKGROUND_COLORS.
+ int color = sIconBackgroundColors[mIconColorIndex];
+ image.setBackgroundColor(color);
+ }
+ }
+
+ private static class DashClockIconCardThumbnailSource implements CardThumbnail.CustomSource {
+ private final static float[] WHITE_COLOR_MATRIX = new float[] {
+ 1f, 1f, 1f, 0, 0,
+ 1f, 1f, 1f, 0, 0,
+ 1f, 1f, 1f, 0, 0,
+ 0, 0, 0, 1f, 0
+ };
+
+ Context mContext;
+ ComponentName mComponentName;
+ ExtensionData mExtensionData;
+ // A String representing the source of this image, specific to dashclock extensions
+ // This String will represent either a URI or a resource id int.
+ private String mImageSource;
+
+ public DashClockIconCardThumbnailSource(Context context,
+ ComponentName componentName,
+ ExtensionData extensionData) {
+ mContext = context;
+ mComponentName = componentName;
+ mExtensionData = extensionData;
+ }
+
+ @Override
+ public String getTag() {
+ return mComponentName.flattenToShortString() + getImageSource();
+ }
+
+ @Override
+ public Bitmap getBitmap() {
+ Bitmap bitmapToReturn;
+ // As per the DashClock documentation, prefer the iconUri resource.
+ if(mExtensionData.iconUri() != null) {
+ bitmapToReturn = getBitmapFromUri(mExtensionData.iconUri());
+ mImageSource = mExtensionData.iconUri().toString();
+ } else {
+ bitmapToReturn = getIconFromResId(mExtensionData.icon());
+ mImageSource = Integer.toString(mExtensionData.icon());
+ }
+ // Return an all white (leaving alpha alone) version of the icon.
+ return applyWhiteColorFilter(bitmapToReturn);
+ }
+
+ private void updateImageSource() {
+ // As per the DashClock documentation, prefer the iconUri resource.
+ if(mExtensionData.iconUri() != null) {
+ mImageSource = mExtensionData.iconUri().toString();
+ } else {
+ mImageSource = Integer.toString(mExtensionData.icon());
+ }
+ }
+
+ public String getImageSource() {
+ updateImageSource();
+ return mImageSource;
+ }
+
+ public void setExtensionData(ExtensionData extensionData) {
+ mExtensionData = extensionData;
+ }
+
+ public boolean shouldUpdateThumbnail() {
+ boolean hasNewUri = (mExtensionData.iconUri() != null
+ && !mExtensionData.iconUri().toString().equals(getImageSource()));
+
+ boolean hasNewResId = !hasNewUri
+ && mExtensionData.icon() > 0
+ && !Integer.toString(mExtensionData.icon()).
+ equals(getImageSource());
+
+ return hasNewUri || hasNewResId;
+ }
+
+ private Bitmap getIconFromResId(int resId) {
+ String packageName = mComponentName.getPackageName();
+ try {
+ Context packageContext = mContext.createPackageContext(packageName, 0);
+ Resources packagesRes = packageContext.getResources();
+ return BitmapFactory.decodeResource(packagesRes, resId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "DashClock icon could not be loaded from package: " + packageName);
+ }
+ return null;
+ }
+
+ private Bitmap getBitmapFromUri(Uri uri) {
+ ParcelFileDescriptor iconFd;
+ Bitmap icon = null;
+ try {
+ iconFd = mContext.getContentResolver().openFileDescriptor(uri, "r");
+ icon = BitmapFactory.decodeFileDescriptor(iconFd.getFileDescriptor());
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "DashClock icon could not be loaded: " + uri);
+ }
+ return icon;
+ }
+
+ /**
+ * The DashClock extension docs say that icons should be all white
+ * with a transparent background, but I have found that many do not
+ * respect this. This method corrects that by changing any non-transparent pixel to
+ * white, leaving alpha values alone.
+ * @param bitmap The input bitmap to color.
+ * @return A copy of the original bitmap, colored to white.
+ */
+ private Bitmap applyWhiteColorFilter(Bitmap bitmap) {
+ Bitmap mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
+ Paint paint = new Paint();
+ ColorMatrixColorFilter matrixColorFilter = new ColorMatrixColorFilter(WHITE_COLOR_MATRIX);
+ paint.setColorFilter(matrixColorFilter);
+ Canvas canvas = new Canvas(mutableBitmap);
+ canvas.drawBitmap(mutableBitmap, 0, 0, paint);
+ return mutableBitmap;
+ }
+ }
+
+ private class DashClockCardExpand extends CardExpand {
+ Context mContext;
+ boolean mSettingsEnabled = false;
+ boolean mActionEnabled = false;
+
+ public DashClockCardExpand(Context context) {
+ super(context, R.layout.dashclock_card_expand_inner_content);
+ mContext = context;
+ }
+
+ private Intent getClickIntent() {
+ Intent clickIntent = null;
+ if(mExtensionWithData.latestData.clickIntent() != null) {
+ clickIntent = mExtensionWithData.latestData.clickIntent();
+ clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ return clickIntent;
+ }
+
+ private Intent getSettingsIntent() {
+ Intent settingsIntent = null;
+ if (mExtensionWithData.listing.settingsActivity != null) {
+ settingsIntent = new Intent();
+ settingsIntent.setComponent(mExtensionWithData.listing.settingsActivity);
+ settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ return settingsIntent;
+ }
+
+ public boolean getSettingsEnabled() {
+ return mSettingsEnabled;
+ }
+
+ public boolean getActionEnabled() {
+ return mActionEnabled;
+ }
+
+ public void onExtensionUpdate() {
+ mActionEnabled = isIntentSupported(mContext, getClickIntent());
+ mSettingsEnabled = isIntentSupported(mContext, getSettingsIntent());
+ }
+
+ private void showNoActivityFoundToast() {
+ String message =
+ mContext.getResources().
+ getString(R.string.dashclock_activity_not_found_toast_message);
+ Toast.makeText(mHostActivityContext,
+ message,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View view) {
+ if (view == null) return;
+
+ Button clickButton =
+ (Button) view.findViewById(R.id.dashclock_card_expand_action_button);
+
+ if (clickButton != null) {
+ if(!isIntentSupported(mContext, getClickIntent())) {
+ mActionEnabled = false;
+ clickButton.setVisibility(View.GONE);
+ } else {
+ mActionEnabled = true;
+ clickButton.setVisibility(View.VISIBLE);
+ clickButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent clickIntent = getClickIntent();
+ if (clickIntent != null) {
+ startIntentIfSupported(clickIntent);
+ }
+ }
+ });
+ }
+ }
+
+ Button settingsButton =
+ (Button) view.findViewById(R.id.dashclock_card_expand_settings_button);
+
+ if (settingsButton != null) {
+ if(!isIntentSupported(mContext, getSettingsIntent())) {
+ mSettingsEnabled = false;
+ settingsButton.setVisibility(View.GONE);
+ } else {
+ mSettingsEnabled = true;
+ settingsButton.setVisibility(View.VISIBLE);
+ settingsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent settingsIntent = getSettingsIntent();
+ if (settingsIntent != null) {
+ startIntentIfSupported(settingsIntent);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void startIntentIfSupported(Intent intent) {
+ try {
+ if (isIntentSupported(mContext, intent)) {
+ mContext.startActivity(intent);
+ } else {
+ showNoActivityFoundToast();
+ }
+ } catch (ActivityNotFoundException e) {
+ showNoActivityFoundToast();
+ } catch (SecurityException e) {
+ // The extension linked to an Activity that
+ // we don't have permission to launch
+ showNoActivityFoundToast();
+ }
+ }
+
+ private boolean isIntentSupported(Context context, Intent intent) {
+ if(intent == null || context == null) {
+ return false;
+ }
+
+ PackageManager pm = context.getPackageManager();
+ return pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null;
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/cards/SimpleMessageCard.java b/src/org/cyanogenmod/launcher/cards/SimpleMessageCard.java
new file mode 100644
index 000000000..97665db33
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/cards/SimpleMessageCard.java
@@ -0,0 +1,55 @@
+package org.cyanogenmod.launcher.cards;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+import it.gmariotti.cardslib.library.internal.Card;
+
+/**
+ * A custom card that will show a title and message only.
+ * Swipe is also enabled by default.
+ */
+public class SimpleMessageCard extends CmCard {
+ private String mBody;
+
+ public SimpleMessageCard(Context context) {
+ this(context, R.layout.simple_message_card_inner_content);
+ }
+
+ public SimpleMessageCard(final Context context, int innerLayout) {
+ super(context, innerLayout);
+ setSwipeable(true);
+ }
+
+ @Override
+ public void onUndoSwipe(Card card, boolean timedOut) {
+ // TODO implement undo handling
+ }
+
+ public void setBody(String body) {
+ mBody = body;
+ }
+
+ public String getBody() {
+ return mBody;
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View view) {
+ TextView title = (TextView)view.findViewById(R.id.simple_message_card_title);
+ TextView body = (TextView)view.findViewById(R.id.simple_message_card_text);
+
+ if (!TextUtils.isEmpty(getTitle())) {
+ title.setText(getTitle());
+ }
+ if (!TextUtils.isEmpty(getBody())) {
+ body.setText(getBody());
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/cards/StatusCard.java b/src/org/cyanogenmod/launcher/cards/StatusCard.java
new file mode 100644
index 000000000..0645336ba
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/cards/StatusCard.java
@@ -0,0 +1,52 @@
+package org.cyanogenmod.launcher.cards;
+
+import android.content.Context;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.launcher3.R;
+import it.gmariotti.cardslib.library.internal.Card;
+import org.cyanogenmod.launcher.home.api.cards.CardData;
+
+/**
+ * A card for a text based message, such as a social media status.
+ */
+public class StatusCard extends ApiCard {
+ private String mStatus = "";
+
+ public StatusCard(Context context, CardData cardData) {
+ this(context, R.layout.status_card_inner_content, cardData);
+ StatusCardHeader header = new StatusCardHeader(context, cardData);
+ addCardHeader(header);
+ setStatus(cardData.getBodyText());
+ }
+
+ public StatusCard(final Context context, int innerLayout, CardData cardData) {
+ super(context, innerLayout, cardData);
+ setSwipeable(true);
+ }
+
+ @Override
+ public void onUndoSwipe(Card card, boolean timedOut) {
+ // TODO implement undo handling
+ }
+
+ private void setStatus(String status) {
+ mStatus = status;
+ }
+
+ public String getStatus() {
+ return mStatus;
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View view) {
+ TextView status = (TextView)view.findViewById(R.id.status_card_status_text);
+
+ if (status != null) {
+ status.setText(Html.fromHtml(getStatus()));
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/cards/StatusCardHeader.java b/src/org/cyanogenmod/launcher/cards/StatusCardHeader.java
new file mode 100644
index 000000000..c8d2f22bc
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/cards/StatusCardHeader.java
@@ -0,0 +1,53 @@
+package org.cyanogenmod.launcher.cards;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.launcher3.R;
+import it.gmariotti.cardslib.library.internal.CardHeader;
+import org.cyanogenmod.launcher.home.api.cards.CardData;
+
+import java.util.Date;
+
+/**
+ * The header for a status card, backed by a CM Home API CardData.
+ */
+public class StatusCardHeader extends CardHeader {
+ private CardData mCardData;
+
+ public StatusCardHeader(Context context, CardData cardData) {
+ super(context, R.layout.card_status_header_inner);
+ setCardData(cardData);
+ }
+
+ private void setCardData(CardData cardData) {
+ mCardData = cardData;
+ }
+
+ private CardData getCardData() {
+ return mCardData;
+ }
+
+ @Override
+ public void setupInnerViewElements(ViewGroup parent, View view) {
+ if (view != null && getCardData() != null) {
+ TextView titleTv = (TextView) view.findViewById(R.id.status_card_title);
+ String title = getCardData().getTitle();
+ if (titleTv != null && !TextUtils.isEmpty(title)) {
+ titleTv.setText(title);
+ }
+
+ TextView dateTv = (TextView) view.findViewById(R.id.status_card_date);
+ Date contentCreatedDate = getCardData().getContentCreatedDate();
+ String dateString = DateUtils.getRelativeTimeSpanString(contentCreatedDate.getTime(),
+ System.currentTimeMillis(),
+ DateUtils.SECOND_IN_MILLIS).toString();
+ if (dateTv != null && !TextUtils.isEmpty(dateString)) {
+ dateTv.setText(dateString);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java
new file mode 100644
index 000000000..1bf2bb9e3
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.launcher.dashclock;
+
+import com.google.android.apps.dashclock.api.DashClockExtension;
+import com.google.android.apps.dashclock.api.ExtensionData;
+import com.google.android.apps.dashclock.api.internal.IExtension;
+import com.google.android.apps.dashclock.api.internal.IExtensionHost;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * The primary local-process endpoint that deals with extensions. Instances of this class are in
+ * charge of maintaining a {@link ServiceConnection} with connected extensions. There should
+ * only be one instance of this class in the app.
+ * <p>
+ * This class is intended to be used as part of a containing service. Make sure to call
+ * {@link #destroy()} in the service's {@link android.app.Service#onDestroy()}.
+ */
+public class ExtensionHost {
+ // TODO: this class badly needs inline docs
+ private static final String TAG = "ExtensionHost";
+
+ private static final int CURRENT_EXTENSION_PROTOCOL_VERSION = 2;
+
+ /**
+ * The amount of time to wait after something has changed before recognizing it as an individual
+ * event. Any changes within this time window will be collapsed, and will further delay the
+ * handling of the event.
+ */
+ public static final int UPDATE_COLLAPSE_TIME_MILLIS = 500;
+
+ private Context mContext;
+ private Context mHostActivityContext;
+ private Handler mClientThreadHandler = new Handler();
+
+ private ExtensionManager mExtensionManager;
+
+ private Map<ComponentName, Connection> mExtensionConnections
+ = new HashMap<ComponentName, Connection>();
+
+ private final Set<ComponentName> mExtensionsToUpdateWhenScreenOn = new HashSet<ComponentName>();
+ private boolean mScreenOnReceiverRegistered = false;
+
+ private volatile Looper mAsyncLooper;
+ private volatile Handler mAsyncHandler;
+
+ public ExtensionHost(Context context, Context hostActivityContext) {
+ mContext = context;
+ mHostActivityContext = hostActivityContext;
+ init();
+ }
+
+ public void init() {
+ mExtensionManager = ExtensionManager.getInstance(mContext, mHostActivityContext);
+ mExtensionManager.addOnChangeListener(mChangeListener);
+
+ HandlerThread thread = new HandlerThread("ExtensionHost");
+ thread.start();
+ mAsyncLooper = thread.getLooper();
+ mAsyncHandler = new Handler(mAsyncLooper);
+
+ mChangeListener.onExtensionsChanged(null);
+ mExtensionManager.cleanupExtensions();
+
+ Log.d(TAG, "ExtensionHost initialized.");
+ }
+
+ public void destroy() {
+ mExtensionManager.removeOnChangeListener(mChangeListener);
+ if (mScreenOnReceiverRegistered) {
+ mContext.unregisterReceiver(mScreenOnReceiver);
+ mScreenOnReceiverRegistered = false;
+ }
+ establishAndDestroyConnections(new ArrayList<ComponentName>());
+ mAsyncLooper.quit();
+ }
+
+ private void establishAndDestroyConnections(List<ComponentName> newExtensionNames) {
+ // Get the list of active extensions
+ Set<ComponentName> activeSet = new HashSet<ComponentName>();
+ activeSet.addAll(newExtensionNames);
+
+ // Get the list of connected extensions
+ Set<ComponentName> connectedSet = new HashSet<ComponentName>();
+ connectedSet.addAll(mExtensionConnections.keySet());
+
+ for (final ComponentName cn : activeSet) {
+ if (connectedSet.contains(cn)) {
+ continue;
+ }
+
+ // Bind anything not currently connected (this is the initial connection
+ // to the now-added extension)
+ Connection conn = createConnection(cn, false);
+ if (conn != null) {
+ mExtensionConnections.put(cn, conn);
+ }
+ }
+
+ // Remove active items from the connected set, leaving only newly-inactive items
+ // to be disconnected below.
+ connectedSet.removeAll(activeSet);
+
+ for (ComponentName cn : connectedSet) {
+ Connection conn = mExtensionConnections.get(cn);
+
+ // Unbind the now-disconnected extension
+ destroyConnection(conn);
+ mExtensionConnections.remove(cn);
+ }
+ }
+
+ private Connection createConnection(final ComponentName cn, final boolean isReconnect) {
+ Log.d(TAG, "createConnection for " + cn + "; isReconnect=" + isReconnect);
+
+ final Connection conn = new Connection();
+ conn.componentName = cn;
+ conn.contentObserver = new ContentObserver(mClientThreadHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ execute(conn.componentName,
+ UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_CONTENT_CHANGED),
+ UPDATE_COLLAPSE_TIME_MILLIS,
+ DashClockExtension.UPDATE_REASON_CONTENT_CHANGED);
+ }
+ };
+ conn.hostInterface = makeHostInterface(conn);
+ conn.serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(final ComponentName componentName, IBinder iBinder) {
+ conn.ready = true;
+ conn.binder = IExtension.Stub.asInterface(iBinder);
+
+ // Initialize the service
+ execute(conn, new Operation() {
+ @Override
+ public void run(IExtension extension) throws RemoteException {
+ // Note that this is protected from ANRs since it runs in the
+ // AsyncHandler thread. Also, since this is a 'oneway' call,
+ // when used with remote extensions, this call does not block.
+ try {
+ extension.onInitialize(conn.hostInterface, isReconnect);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Error initializing extension "
+ + componentName.toString(), e);
+ }
+ }
+ }, 0, null);
+
+ if (!isReconnect) {
+ execute(conn.componentName,
+ UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_INITIAL),
+ 0,
+ null);
+ }
+
+ // Execute operations that were deferred until the service was available.
+ // TODO: handle service disruptions that occur here
+ synchronized (conn.deferredOps) {
+ if (conn.ready) {
+ Set<Object> processedCollapsedTokens = new HashSet<Object>();
+ Iterator<Pair<Object, Operation>> it = conn.deferredOps.iterator();
+ while (it.hasNext()) {
+ Pair<Object, Operation> op = it.next();
+ if (op.first != null) {
+ if (processedCollapsedTokens.contains(op.first)) {
+ // An operation with this collapse token has already been
+ // processed; skip this one.
+ continue;
+ }
+
+ processedCollapsedTokens.add(op.first);
+ }
+ execute(conn, op.second, 0, null);
+ it.remove();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName componentName) {
+ conn.serviceConnection = null;
+ conn.binder = null;
+ conn.ready = false;
+ mClientThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mExtensionConnections.remove(componentName);
+ }
+ });
+ }
+ };
+
+ try {
+ if (!mContext.bindService(new Intent().setComponent(cn), conn.serviceConnection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Error binding to extension " + cn.flattenToShortString());
+ return null;
+ }
+ } catch (SecurityException e) {
+ Log.e(TAG, "Error binding to extension " + cn.flattenToShortString(), e);
+ return null;
+ }
+
+ return conn;
+ }
+
+ private IExtensionHost makeHostInterface(final Connection conn) {
+ return new IExtensionHost.Stub() {
+ @Override
+ public void publishUpdate(ExtensionData data) throws RemoteException {
+ if (data == null) {
+ data = new ExtensionData();
+ }
+
+ // TODO: this needs to be thread-safe
+ Log.d(TAG, "publishUpdate received for extension " + conn.componentName);
+ mExtensionManager.updateExtensionData(conn.componentName, data);
+ }
+
+ @Override
+ public void addWatchContentUris(String[] contentUris) throws RemoteException {
+ if (contentUris != null && contentUris.length > 0 && conn.contentObserver != null) {
+ ContentResolver resolver = mContext.getContentResolver();
+ for (String uri : contentUris) {
+ if (TextUtils.isEmpty(uri)) {
+ continue;
+ }
+
+ resolver.registerContentObserver(Uri.parse(uri), true,
+ conn.contentObserver);
+ }
+ }
+ }
+
+ @Override
+ public void removeAllWatchContentUris() throws RemoteException {
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.unregisterContentObserver(conn.contentObserver);
+ }
+
+ @Override
+ public void setUpdateWhenScreenOn(boolean updateWhenScreenOn) throws RemoteException {
+ synchronized (mExtensionsToUpdateWhenScreenOn) {
+ if (updateWhenScreenOn) {
+ if (mExtensionsToUpdateWhenScreenOn.size() == 0) {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mScreenOnReceiver, filter);
+ mScreenOnReceiverRegistered = true;
+ }
+
+ mExtensionsToUpdateWhenScreenOn.add(conn.componentName);
+
+ } else {
+ mExtensionsToUpdateWhenScreenOn.remove(conn.componentName);
+
+ if (mExtensionsToUpdateWhenScreenOn.size() == 0) {
+ mContext.unregisterReceiver(mScreenOnReceiver);
+ mScreenOnReceiverRegistered = false;
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public void requestAllManualUpdate() {
+ for (ComponentName cn : mExtensionConnections.keySet()) {
+ execute(cn, UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_MANUAL),
+ 0, null);
+ }
+ }
+
+ private void destroyConnection(Connection conn) {
+ if (conn.contentObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(conn.contentObserver);
+ conn.contentObserver = null;
+ }
+
+ conn.binder = null;
+ mContext.unbindService(conn.serviceConnection);
+ conn.serviceConnection = null;
+ }
+
+ private ExtensionManager.OnChangeListener mChangeListener
+ = new ExtensionManager.OnChangeListener() {
+ @Override
+ public void onExtensionsChanged(ComponentName sourceExtension) {
+ if (sourceExtension != null) {
+ // If the extension change is a result of a single extension, don't do anything,
+ // since we're only interested in events triggered by the system overall (e.g.
+ // extensions added or removed).
+ return;
+ }
+ Log.d(TAG, "onExtensionsChanged; calling establishAndDestroyConnections.");
+ establishAndDestroyConnections(mExtensionManager.getActiveExtensionNames());
+ }
+ };
+
+ private void execute(final Connection conn, final Operation operation,
+ int collapseDelayMillis, final Object collapseToken) {
+ final Object collapseTokenForConn;
+ if (collapseDelayMillis > 0 && collapseToken != null) {
+ collapseTokenForConn = new Pair<ComponentName, Object>(conn.componentName,
+ collapseToken);
+ } else {
+ collapseTokenForConn = null;
+ }
+
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (conn.binder == null) {
+ throw new RemoteException("Binder is unavailable.");
+ }
+ operation.run(conn.binder);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't execute operation; scheduling for retry upon service "
+ + "reconnection.", e);
+ // TODO: exponential backoff for retrying the same operation, or fail after
+ // n attempts (in case the remote service consistently crashes when
+ // executing this operation)
+ synchronized (conn.deferredOps) {
+ conn.deferredOps.add(new Pair<Object, Operation>(
+ collapseTokenForConn, operation));
+ }
+ }
+ }
+ };
+
+ if (conn.ready) {
+ if (collapseTokenForConn != null) {
+ mAsyncHandler.removeCallbacksAndMessages(collapseTokenForConn);
+ }
+
+ if (collapseDelayMillis > 0) {
+ mAsyncHandler.postAtTime(runnable, collapseTokenForConn,
+ SystemClock.uptimeMillis() + collapseDelayMillis);
+ } else {
+ mAsyncHandler.post(runnable);
+ }
+ } else {
+ mAsyncHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (conn.deferredOps) {
+ conn.deferredOps.add(new Pair<Object, Operation>(
+ collapseTokenForConn, operation));
+ }
+ }
+ });
+ }
+ }
+
+ public void execute(ComponentName cn, Operation operation,
+ int collapseDelayMillis, final Object collapseToken) {
+ Connection conn = mExtensionConnections.get(cn);
+ if (conn == null) {
+ conn = createConnection(cn, true);
+ if (conn != null) {
+ mExtensionConnections.put(cn, conn);
+ } else {
+ Log.e(TAG, "Couldn't connect to extension to perform operation; operation "
+ + "canceled.");
+ return;
+ }
+ }
+
+ execute(conn, operation, collapseDelayMillis, collapseToken);
+ }
+
+ private final BroadcastReceiver mScreenOnReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mExtensionsToUpdateWhenScreenOn) {
+ for (ComponentName cn : mExtensionsToUpdateWhenScreenOn) {
+ execute(cn, UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_SCREEN_ON),
+ 0, null);
+ }
+ }
+ }
+ };
+
+ static final SparseArray<Operation> UPDATE_OPERATIONS = new SparseArray<Operation>();
+
+ static {
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_UNKNOWN);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_INITIAL);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_PERIODIC);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_SETTINGS_CHANGED);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_CONTENT_CHANGED);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_SCREEN_ON);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_MANUAL);
+ }
+
+ private static void _createUpdateOperation(final int reason) {
+ UPDATE_OPERATIONS.put(reason, new ExtensionHost.Operation() {
+ @Override
+ public void run(IExtension extension) throws RemoteException {
+ // Note that this is protected from ANRs since it runs in the AsyncHandler thread.
+ // Also, since this is a 'oneway' call, when used with remote extensions, this call
+ // does not block.
+ extension.onUpdate(reason);
+ }
+ });
+ }
+
+ public static boolean supportsProtocolVersion(int protocolVersion) {
+ return protocolVersion > 0 && protocolVersion <= CURRENT_EXTENSION_PROTOCOL_VERSION;
+ }
+
+ /**
+ * Will be run on a worker thread.
+ */
+ public static interface Operation {
+ void run(IExtension extension) throws RemoteException;
+ }
+
+ private static class Connection {
+ boolean ready = false;
+ ComponentName componentName;
+ ServiceConnection serviceConnection;
+ IExtension binder;
+ IExtensionHost hostInterface;
+ ContentObserver contentObserver;
+
+ /**
+ * Only access on the async thread. The pair is (collapse token, operation)
+ */
+ final Queue<Pair<Object, Operation>> deferredOps
+ = new LinkedList<Pair<Object, Operation>>();
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java
new file mode 100644
index 000000000..a64b0be2e
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Modified 2014 for the CyanogenMod Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.cyanogenmod.launcher.dashclock;
+
+import android.app.backup.BackupManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.android.apps.dashclock.api.DashClockExtension;
+import com.google.android.apps.dashclock.api.ExtensionData;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A singleton class in charge of extension registration, activation (change in user-specified
+ * 'active' extensions), and data caching.
+ */
+public class ExtensionManager {
+ private static final String TAG = "ExtensionManager";
+
+ private static final String PREF_ACTIVE_EXTENSIONS = "active_extensions";
+
+ // No default extensions for now. TODO: include dashclock's default extensions
+ private static final Class[] DEFAULT_EXTENSIONS = {};
+
+ private final Context mContext;
+
+ private final List<ExtensionWithData> mActiveExtensions = new ArrayList<ExtensionWithData>();
+ private Map<ComponentName, ExtensionWithData> mExtensionInfoMap
+ = new HashMap<ComponentName, ExtensionWithData>();
+ private List<OnChangeListener> mOnChangeListeners = new ArrayList<OnChangeListener>();
+
+ private SharedPreferences mDefaultPreferences;
+ private SharedPreferences mValuesPreferences;
+ private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ private static ExtensionManager sInstance;
+
+ public static ExtensionManager getInstance(Context context, Context hostActivityContext) {
+ if (sInstance == null) {
+ sInstance = new ExtensionManager(context, hostActivityContext);
+ }
+
+ return sInstance;
+ }
+
+ private ExtensionManager(Context context, Context hostActivityContext) {
+ mContext = context;
+ mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(hostActivityContext);
+ mValuesPreferences = hostActivityContext.getSharedPreferences("extension_data", 0);
+ loadActiveExtensionList();
+ }
+
+ /**
+ * De-activates active extensions that are unsupported or are no longer installed.
+ */
+ public boolean cleanupExtensions() {
+ Set<ComponentName> availableExtensions = new HashSet<ComponentName>();
+ for (ExtensionListing listing : getAvailableExtensions()) {
+ // Ensure the extension protocol version is supported. If it isn't, don't allow its use.
+ if (!ExtensionHost.supportsProtocolVersion(listing.protocolVersion)) {
+ Log.w(TAG, "Extension '" + listing.title + "' using unsupported protocol version "
+ + listing.protocolVersion + ".");
+ continue;
+ }
+ availableExtensions.add(listing.componentName);
+ }
+
+ boolean cleanupRequired = false;
+ ArrayList<ComponentName> newActiveExtensions = new ArrayList<ComponentName>();
+
+ synchronized (mActiveExtensions) {
+ for (ExtensionWithData ewd : mActiveExtensions) {
+ if (availableExtensions.contains(ewd.listing.componentName)) {
+ newActiveExtensions.add(ewd.listing.componentName);
+ } else {
+ cleanupRequired = true;
+ }
+ }
+ }
+
+ if (cleanupRequired) {
+ setActiveExtensions(newActiveExtensions);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void loadActiveExtensionList() {
+ List<ComponentName> activeExtensions = new ArrayList<ComponentName>();
+ String extensions;
+ if (mDefaultPreferences.contains(PREF_ACTIVE_EXTENSIONS)) {
+ extensions = mDefaultPreferences.getString(PREF_ACTIVE_EXTENSIONS, "");
+ } else {
+ extensions = createDefaultExtensionList();
+ }
+ String[] componentNameStrings = extensions.split(",");
+ for (String componentNameString : componentNameStrings) {
+ if (TextUtils.isEmpty(componentNameString)) {
+ continue;
+ }
+ activeExtensions.add(ComponentName.unflattenFromString(componentNameString));
+ }
+ setActiveExtensions(activeExtensions, false);
+ }
+
+ private String createDefaultExtensionList() {
+ StringBuilder sb = new StringBuilder();
+
+ for (Class cls : DEFAULT_EXTENSIONS) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(new ComponentName(mContext, cls).flattenToString());
+ }
+
+ return sb.toString();
+ }
+
+ private void saveActiveExtensionList() {
+ StringBuilder sb = new StringBuilder();
+
+ synchronized (mActiveExtensions) {
+ for (ExtensionWithData ci : mActiveExtensions) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(ci.listing.componentName.flattenToString());
+ }
+ }
+
+ mDefaultPreferences.edit()
+ .putString(PREF_ACTIVE_EXTENSIONS, sb.toString())
+ .commit();
+ new BackupManager(mContext).dataChanged();
+ }
+
+ /**
+ * Replaces the set of active extensions with the given list.
+ */
+ public void setActiveExtensions(List<ComponentName> extensions) {
+ setActiveExtensions(extensions, true);
+ }
+
+ private void setActiveExtensions(List<ComponentName> extensionNames, boolean saveAndNotify) {
+ Map<ComponentName, ExtensionListing> listings
+ = new HashMap<ComponentName, ExtensionListing>();
+ for (ExtensionListing listing : getAvailableExtensions()) {
+ listings.put(listing.componentName, listing);
+ }
+
+ List<ComponentName> activeExtensionNames = getActiveExtensionNames();
+ if (activeExtensionNames.equals(extensionNames)) {
+ Log.d(TAG, "No change to list of active extensions.");
+ return;
+ }
+
+ // Clear cached data for any no-longer-active extensions.
+ for (ComponentName cn : activeExtensionNames) {
+ if (!extensionNames.contains(cn)) {
+ destroyExtensionData(cn);
+ }
+ }
+
+ // Set the new list of active extensions, loading cached data if necessary.
+ List<ExtensionWithData> newActiveExtensions = new ArrayList<ExtensionWithData>();
+
+ for (ComponentName cn : extensionNames) {
+ if (mExtensionInfoMap.containsKey(cn)) {
+ newActiveExtensions.add(mExtensionInfoMap.get(cn));
+ } else {
+ ExtensionWithData ewd = new ExtensionWithData();
+ ewd.listing = listings.get(cn);
+ if (ewd.listing == null) {
+ ewd.listing = new ExtensionListing();
+ ewd.listing.componentName = cn;
+ }
+ ewd.latestData = deserializeExtensionData(ewd.listing.componentName);
+ newActiveExtensions.add(ewd);
+ }
+ }
+
+ mExtensionInfoMap.clear();
+ for (ExtensionWithData ewd : newActiveExtensions) {
+ mExtensionInfoMap.put(ewd.listing.componentName, ewd);
+ }
+
+ synchronized (mActiveExtensions) {
+ mActiveExtensions.clear();
+ mActiveExtensions.addAll(newActiveExtensions);
+ }
+
+ if (saveAndNotify) {
+ Log.d(TAG, "List of active extensions has changed.");
+ saveActiveExtensionList();
+ notifyOnChangeListeners(null);
+ }
+ }
+
+ /**
+ * Updates and caches the user-visible data for a given extension.
+ */
+ public boolean updateExtensionData(ComponentName cn, ExtensionData data) {
+ data.clean();
+
+ ExtensionWithData ewd = mExtensionInfoMap.get(cn);
+ if (ewd != null && !ExtensionData.equals(ewd.latestData, data)) {
+ ewd.latestData = data;
+ serializeExtensionData(ewd.listing.componentName, data);
+ notifyOnChangeListeners(ewd.listing.componentName);
+ return true;
+ }
+ return false;
+ }
+
+ private ExtensionData deserializeExtensionData(ComponentName componentName) {
+ ExtensionData extensionData = new ExtensionData();
+ String val = mValuesPreferences.getString(componentName.flattenToString(), "");
+ if (!TextUtils.isEmpty(val)) {
+ try {
+ extensionData.deserialize((JSONObject) new JSONTokener(val).nextValue());
+ } catch (JSONException e) {
+ Log.e(TAG, "Error loading extension data cache for " + componentName + ".",
+ e);
+ }
+ }
+ return extensionData;
+ }
+
+ private void serializeExtensionData(ComponentName componentName, ExtensionData extensionData) {
+ try {
+ mValuesPreferences.edit()
+ .putString(componentName.flattenToString(),
+ extensionData.serialize().toString())
+ .apply();
+ } catch (JSONException e) {
+ Log.e(TAG, "Error storing extension data cache for " + componentName + ".", e);
+ }
+ }
+
+ private void destroyExtensionData(ComponentName componentName) {
+ mValuesPreferences.edit()
+ .remove(componentName.flattenToString())
+ .apply();
+ }
+
+ public List<ExtensionWithData> getActiveExtensionsWithData() {
+ ArrayList<ExtensionWithData> activeExtensions;
+ synchronized (mActiveExtensions) {
+ activeExtensions = new ArrayList<ExtensionWithData>(mActiveExtensions);
+ }
+ return activeExtensions;
+ }
+
+ public List<ExtensionWithData> getVisibleExtensionsWithData() {
+ ArrayList<ExtensionWithData> visibleExtensions = new ArrayList<ExtensionWithData>();
+ synchronized (mActiveExtensions) {
+ for (ExtensionManager.ExtensionWithData ewd : mActiveExtensions) {
+ if (ewd.latestData.visible()) {
+ visibleExtensions.add(ewd);
+ }
+ }
+ }
+ return visibleExtensions;
+ }
+
+ public List<ComponentName> getActiveExtensionNames() {
+ List<ComponentName> list = new ArrayList<ComponentName>();
+ for (ExtensionWithData ci : mActiveExtensions) {
+ list.add(ci.listing.componentName);
+ }
+ return list;
+ }
+
+ /**
+ * Returns a listing of all available (installed) extensions.
+ */
+ public List<ExtensionListing> getAvailableExtensions() {
+ List<ExtensionListing> availableExtensions = new ArrayList<ExtensionListing>();
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> resolveInfos = pm.queryIntentServices(
+ new Intent(DashClockExtension.ACTION_EXTENSION), PackageManager.GET_META_DATA);
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ExtensionListing listing = new ExtensionListing();
+ listing.componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
+ resolveInfo.serviceInfo.name);
+ listing.title = resolveInfo.loadLabel(pm).toString();
+ Bundle metaData = resolveInfo.serviceInfo.metaData;
+ if (metaData != null) {
+ listing.protocolVersion = metaData.getInt("protocolVersion");
+ listing.worldReadable = metaData.getBoolean("worldReadable", false);
+ listing.description = metaData.getString("description");
+ String settingsActivity = metaData.getString("settingsActivity");
+ if (!TextUtils.isEmpty(settingsActivity)) {
+ listing.settingsActivity = ComponentName.unflattenFromString(
+ resolveInfo.serviceInfo.packageName + "/" + settingsActivity);
+ }
+ }
+
+ listing.icon = resolveInfo.loadIcon(pm);
+ availableExtensions.add(listing);
+ }
+
+ return availableExtensions;
+ }
+
+ /**
+ * Registers a listener to be triggered when either the list of active extensions changes or an
+ * extension's data changes.
+ */
+ public void addOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.add(onChangeListener);
+ }
+
+ /**
+ * Removes a listener previously registered with {@link #addOnChangeListener}.
+ */
+ public void removeOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.remove(onChangeListener);
+ }
+
+ private void notifyOnChangeListeners(final ComponentName sourceExtension) {
+ mMainThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (OnChangeListener listener : mOnChangeListeners) {
+ listener.onExtensionsChanged(sourceExtension);
+ }
+ }
+ });
+ }
+
+ public static interface OnChangeListener {
+ /**
+ * @param sourceExtension null if not related to any specific extension (e.g. list of
+ * extensions has changed).
+ */
+ void onExtensionsChanged(ComponentName sourceExtension);
+ }
+
+ public static class ExtensionWithData {
+ public ExtensionListing listing;
+ public ExtensionData latestData;
+ }
+
+ public static class ExtensionListing {
+ public ComponentName componentName;
+ public int protocolVersion;
+ public boolean worldReadable;
+ public String title;
+ public String description;
+ public Drawable icon;
+ public ComponentName settingsActivity;
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java
new file mode 100644
index 000000000..ac32d1e20
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.launcher.dashclock;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.WakefulBroadcastReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Broadcast receiver used to watch for changes to installed packages on the device. This triggers
+ * a cleanup of extensions (in case one was uninstalled), or a data update request to an extension
+ * if it was updated (its package was replaced).
+ */
+public class ExtensionPackageChangeReceiver extends WakefulBroadcastReceiver {
+ private static final String TAG = "ExtensionPackageChangeReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ /*
+ ExtensionManager extensionManager = ExtensionManager.getInstance(context);
+ if (extensionManager.cleanupExtensions()) {
+ Log.d(TAG, "Extension cleanup performed and action taken.");
+
+ TODO Update CMHome with new extension info
+ Intent widgetUpdateIntent = new Intent(context, DashClockService.class);
+ widgetUpdateIntent.setAction(DashClockService.ACTION_UPDATE_WIDGETS);
+ startWakefulService(context, widgetUpdateIntent);
+ }
+
+ // If this is a replacement or change in the package, update all active extensions from
+ // this package.
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+ || Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (TextUtils.isEmpty(packageName)) {
+ return;
+ }
+
+ List<ComponentName> activeExtensions = extensionManager.getActiveExtensionNames();
+ for (ComponentName cn : activeExtensions) {
+ if (packageName.equals(cn.getPackageName())) {
+ /*
+ TODO Update CMHome with new extension info
+ LOGD(TAG, "Package for extension " + cn + " changed; asking it for an update.");
+ Intent extensionUpdateIntent = new Intent(context, DashClockService.class);
+ extensionUpdateIntent.setAction(DashClockService.ACTION_UPDATE_EXTENSIONS);
+ extensionUpdateIntent.putExtra(DashClockService.EXTRA_COMPONENT_NAME,
+ cn.flattenToShortString());
+ startWakefulService(context, extensionUpdateIntent);
+ }
+ }
+ }
+ */
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/home/CMHomeAdapter.java b/src/org/cyanogenmod/launcher/home/CMHomeAdapter.java
new file mode 100644
index 000000000..76656d2a1
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/CMHomeAdapter.java
@@ -0,0 +1,252 @@
+package org.cyanogenmod.launcher.home;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.Image;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
+import org.w3c.dom.Text;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Created by Schoen on 7/25/15.
+ */
+public class CMHomeAdapter extends RecyclerView.Adapter<CMHomeAdapter.ViewHolder>{
+
+ Context mContext;
+ List<CMHomeCard> mCards;
+
+ public static class ViewHolder extends RecyclerView.ViewHolder{
+ //contact
+ LinearLayout contactCard;
+ ImageView contactImage1;
+ ImageView contactImage2;
+ ImageView contactImage3;
+ ImageView contactImage4;
+
+ //calendar card
+ LinearLayout calendarCard;
+ LinearLayout eventContainer;
+
+ //news card
+ LinearLayout newsCard;
+ ImageView newsImage;
+ TextView newsTitle;
+ TextView sourceAndTime;
+
+
+ ViewHolder(View cardView, int cardType){
+ super(cardView);
+
+ switch(cardType){
+ case 0:
+ contactCard = (LinearLayout)itemView.findViewById(R.id.contact_card);
+ contactImage1 = (ImageView)itemView.findViewById(R.id.contact_image_one);
+ contactImage2 = (ImageView)itemView.findViewById(R.id.contact_image_two);
+ contactImage3 = (ImageView)itemView.findViewById(R.id.contact_image_three);
+ contactImage4 = (ImageView)itemView.findViewById(R.id.contact_image_four);
+ break;
+ case 1:
+ calendarCard = (LinearLayout)itemView.findViewById(R.id.calendar_card);
+ eventContainer = (LinearLayout)itemView.findViewById(R.id.event_container);
+ break;
+ case 2:
+ newsCard = (LinearLayout)itemView.findViewById(R.id.news_card);
+ newsImage = (ImageView)itemView.findViewById(R.id.news_image);
+ newsTitle = (TextView)itemView.findViewById(R.id.news_title);
+ sourceAndTime = (TextView)itemView.findViewById(R.id.news_source_time);
+ break;
+ case 3:
+ newsCard = (LinearLayout)itemView.findViewById(R.id.news_card);
+ newsImage = (ImageView)itemView.findViewById(R.id.news_image);
+ newsTitle = (TextView)itemView.findViewById(R.id.news_title);
+ sourceAndTime = (TextView)itemView.findViewById(R.id.news_source_time);
+ break;
+ }
+ }
+
+ }
+
+ CMHomeAdapter(Context context, List<CMHomeCard> cards){
+ mContext = context;
+ mCards = cards;
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView){
+ super.onAttachedToRecyclerView(recyclerView);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+
+ Log.w("HAX", "get item view type");
+ //default type
+ int viewType = 3;
+
+ if(position == 0){
+ //contact card
+ viewType = 0;
+ }
+ if(position == 1){
+ //calendar card
+ viewType = 1;
+ }
+ if(position == 2){
+ viewType = 2;
+ }
+
+ return viewType;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int cardType){
+ View v = null;
+
+ switch(cardType){
+ case 0:
+ v = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.contact_card, viewGroup, false);
+ Log.w("HAX","feature first");
+ break;
+ case 1:
+ v = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.calendar_card, viewGroup, false);
+ Log.w("HAX","feature");
+ break;
+ case 2:
+ v = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.news_card_first, viewGroup, false);
+ Log.w("HAX","item first");
+ break;
+ case 3:
+ v = LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.news_card, viewGroup, false);
+ Log.w("HAX","item");
+ break;
+ }
+
+ ViewHolder vh = new ViewHolder(v, cardType);
+
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder vh,int i){
+
+ if(i == 0){
+ setupContact(vh.contactImage1);
+ setupContact(vh.contactImage2);
+ setupContact(vh.contactImage3);
+ setupContact(vh.contactImage4);
+ }
+
+ if(i == 1){
+ int numEvents = getUpcomingEventCount();
+ getEventData();
+
+ for(int e = 0; e < numEvents; e++){
+ createEventEntry(vh.eventContainer, e);
+ }
+
+ }
+
+ if(i > 1){
+ createNewsCard(vh, i);
+ }
+
+
+ Log.w("HAX","we binded");
+ }
+
+ @Override
+ public int getItemCount(){
+ return mCards.size();
+ }
+
+ private void setupContact(View view){
+ ImageView iv = (ImageView)view;
+ iv.setImageResource(R.drawable.persona2);
+ iv.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ //launch the appropriate contact card
+ }
+ });
+ }
+
+ private int getUpcomingEventCount(){
+ //get the upcoming calendar events here
+ //right now I am just returning an int representing the count available that maxes at 3
+
+ return 2;
+ }
+
+ private void getEventData(){
+ //doesn't do anything yet
+ }
+
+ private void createEventEntry(View view, int eventNum){
+ LinearLayout ll = (LinearLayout)view;
+ View v = LayoutInflater.from(mContext).inflate(R.layout.calendar_event_item,ll,false);
+ TextView startTime = (TextView)v.findViewById(R.id.start_time);
+ TextView endTime = (TextView)v.findViewById(R.id.end_time);
+ TextView title = (TextView)v.findViewById(R.id.event_title);
+ TextView location = (TextView)v.findViewById(R.id.event_location);
+ startTime.setText("12:00");
+ endTime.setText("to 1:00");
+ title.setText("Stand Up");
+ location.setText("Conference Room");
+
+ ll.addView(v);
+ }
+
+ private void createNewsCard(ViewHolder vh, int i){
+ new DownloadImageTask(vh.newsImage)
+ .execute("http://slidell-independent.com/wp-content/uploads/2013/01/wsne.jpg");//need to get the url out of the spoof data
+
+ vh.newsTitle.setText("This is a temp title");
+ vh.sourceAndTime.setText("This is a temp source and time");
+
+ }
+
+ private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
+ ImageView bmImage;
+
+ public DownloadImageTask(ImageView bmImage){
+ this.bmImage = bmImage;
+ }
+
+ protected Bitmap doInBackground(String... urls){
+ String urldisplay = urls[0];
+ Bitmap image = null;
+ try {
+ InputStream in = new java.net.URL(urldisplay).openStream();
+ image = BitmapFactory.decodeStream(in);
+ } catch (Exception e) {
+ Log.e("Error", e.getMessage());
+ e.printStackTrace();
+ }
+ return image;
+ }
+ protected void onPostExecute(Bitmap result){
+ bmImage.setImageBitmap(result);
+ }
+ }
+
+}
diff --git a/src/org/cyanogenmod/launcher/home/CMHomeCalendar.java b/src/org/cyanogenmod/launcher/home/CMHomeCalendar.java
new file mode 100644
index 000000000..c150a2dda
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/CMHomeCalendar.java
@@ -0,0 +1,26 @@
+package org.cyanogenmod.launcher.home;
+
+/**
+ * Created by Schoen on 7/24/15.
+ */
+public class CMHomeCalendar extends CMHomeCard {
+
+ long startTime;
+ long endTime;
+ String title;
+ String location;
+
+ public CMHomeCalendar(
+ long startTime,
+ long endTime,
+ String title,
+ String location){
+
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.title = title;
+ this.location = location;
+
+ }
+
+}
diff --git a/src/org/cyanogenmod/launcher/home/CMHomeCard.java b/src/org/cyanogenmod/launcher/home/CMHomeCard.java
new file mode 100644
index 000000000..b2ece81b0
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/CMHomeCard.java
@@ -0,0 +1,7 @@
+package org.cyanogenmod.launcher.home;
+
+/**
+ * Created by Schoen on 7/24/15.
+ */
+public class CMHomeCard {
+}
diff --git a/src/org/cyanogenmod/launcher/home/CMHomeContact.java b/src/org/cyanogenmod/launcher/home/CMHomeContact.java
new file mode 100644
index 000000000..2194f0dc7
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/CMHomeContact.java
@@ -0,0 +1,22 @@
+package org.cyanogenmod.launcher.home;
+
+/**
+ * Created by Schoen on 7/24/15.
+ */
+public class CMHomeContact extends CMHomeCard{
+ String uri1;
+ String uri2;
+ String uri3;
+ String uri4;
+
+ public CMHomeContact(String uri1, String uri2, String uri3, String uri4){
+
+ this.uri1 = uri1;
+ this.uri2 = uri2;
+ this.uri3 = uri3;
+ this.uri4 = uri4;
+
+
+ }
+
+}
diff --git a/src/org/cyanogenmod/launcher/home/CMHomeNews.java b/src/org/cyanogenmod/launcher/home/CMHomeNews.java
new file mode 100644
index 000000000..8cc087ee3
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/CMHomeNews.java
@@ -0,0 +1,29 @@
+package org.cyanogenmod.launcher.home;
+
+/**
+ * Created by Schoen on 7/24/15.
+ */
+public class CMHomeNews extends CMHomeCard {
+
+ String imageURL;
+ String title;
+ String source;
+ long time;
+ String url;
+
+ public CMHomeNews(
+ String imageURL,
+ String title,
+ String source,
+ long time,
+ String url){
+
+ this.imageURL = imageURL;
+ this.title = title;
+ this.source = source;
+ this.time = time;
+ this.url = url;
+
+ }
+
+}
diff --git a/src/org/cyanogenmod/launcher/home/HomeLauncher.java b/src/org/cyanogenmod/launcher/home/HomeLauncher.java
new file mode 100644
index 000000000..84f2388b5
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/HomeLauncher.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.launcher.home;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class HomeLauncher extends Activity {
+
+ private HomeStub mStub;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mStub = new HomeStub();
+ mStub.setHostActivityContext(this);
+ mStub.onStart(this);
+ setContentView(mStub.createCustomView(this));
+ mStub.setShowContent(this, true);
+ mStub.onShow(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mStub.onDestroy(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mStub.onResume(this);
+ mStub.onShow(this);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mStub.onPause(this);
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/home/HomeLayout.java b/src/org/cyanogenmod/launcher/home/HomeLayout.java
new file mode 100644
index 000000000..c83fd94ec
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/HomeLayout.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.launcher.home;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+public class HomeLayout extends LinearLayout {
+
+ public HomeLayout(Context context) {
+ this(context, null, 0);
+ }
+
+ public HomeLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HomeLayout(final Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ }
+
+}
diff --git a/src/org/cyanogenmod/launcher/home/HomeStub.java b/src/org/cyanogenmod/launcher/home/HomeStub.java
new file mode 100644
index 000000000..808fe8e14
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/home/HomeStub.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.launcher.home;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.os.AsyncTask;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import com.android.launcher.home.Home;
+import com.android.launcher3.R;
+import it.gmariotti.cardslib.library.internal.Card;
+import it.gmariotti.cardslib.library.internal.CardArrayAdapter;
+import it.gmariotti.cardslib.library.view.CardListView;
+import it.gmariotti.cardslib.library.view.listener.dismiss.DefaultDismissableManager;
+import org.cyanogenmod.launcher.cardprovider.CmHomeApiCardProvider;
+import org.cyanogenmod.launcher.cardprovider.DashClockExtensionCardProvider;
+import org.cyanogenmod.launcher.cardprovider.ICardProvider;
+import org.cyanogenmod.launcher.cardprovider.ICardProvider.CardProviderUpdateResult;
+import org.cyanogenmod.launcher.cards.CmCard;
+import org.cyanogenmod.launcher.cards.SimpleMessageCard;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HomeStub implements Home {
+
+ private static final String TAG = "HomeStub";
+ private static final String NO_EXTENSIONS_CARD_ID = "noExtensions";
+ private static final String BACKGROUND_THREAD_NAME = "CMHomeBackgroundThread";
+ private HomeLayout mHomeLayout;
+ private RecyclerView mRecyclerView;
+ private Context mHostActivityContext;
+ private Context mCMHomeContext;
+ private boolean mShowContent = false;
+ private SimpleMessageCard mNoExtensionsCard;
+ private List<ICardProvider> mCardProviders = new ArrayList<ICardProvider>();
+ private List<CMHomeCard> mCards;
+ private CMHomeCardArrayAdapter mCardArrayAdapter;
+ private LinearLayoutManager mLayoutManager;
+
+ private HandlerThread mBackgroundHandlerThread;
+ private Handler mBackgroundHandler;
+ private Handler mUiThreadHandler;
+
+ private final AccelerateInterpolator mAlphaInterpolator;
+
+ private final ICardProvider.CardProviderUpdateListener mCardProviderUpdateListener =
+ new ICardProvider.CardProviderUpdateListener() {
+ @Override
+ public boolean onCardProviderUpdate(String cardId, boolean wasPending) {
+ return refreshCard(cardId);
+ }
+
+ @Override
+ public void onCardDelete(String cardId) {
+
+ }
+ };
+
+ private final Runnable mLoadAllCardsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ loadAllCards();
+ }
+ };
+
+ public HomeStub() {
+ super();
+ mAlphaInterpolator = new AccelerateInterpolator();
+ }
+
+ @Override
+ public void setHostActivityContext(Context context) {
+ mHostActivityContext = context;
+ mUiThreadHandler = new Handler(mHostActivityContext.getMainLooper());
+ }
+
+ @Override
+ public void onStart(Context context) {
+ mCMHomeContext = context;
+
+ // Start up a background thread to handle updating.
+ mBackgroundHandlerThread = new HandlerThread(BACKGROUND_THREAD_NAME);
+ mBackgroundHandlerThread.start();
+ mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());
+
+ if(mShowContent) {
+ // Add any providers we wish to include, if we should show content
+ initProvidersIfNeeded(context);
+ }
+ }
+
+ @Override
+ public void setShowContent(Context context, boolean showContent) {
+ mShowContent = showContent;
+ if(mShowContent) {
+ // Add any providers we wish to include, if we should show content
+ initProvidersIfNeeded(context);
+ if(mHomeLayout != null) {
+ loadCardsFromProviders();
+ }
+ } else {
+ for(ICardProvider cardProvider : mCardProviders) {
+ cardProvider.onHide(context);
+ }
+ mCardProviders.clear();
+ if(mHomeLayout != null) {
+ removeAllCards(context);
+ // Make sure that the Undo Bar is hidden if no content is to be shown.
+ hideUndoBar();
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy(Context context) {
+ mHomeLayout = null;
+ }
+
+ @Override
+ public void onResume(Context context) {
+ }
+
+ @Override
+ public void onPause(Context context) {
+ }
+
+ @Override
+ public void onShow(Context context) {
+ if (mHomeLayout != null) {
+ mHomeLayout.setAlpha(1.0f);
+
+ if(mShowContent) {
+ for(ICardProvider cardProvider : mCardProviders) {
+ cardProvider.onShow();
+ cardProvider.requestRefresh();
+ }
+ } else {
+ hideUndoBar();
+ }
+ }
+ }
+
+ @Override
+ public void onScrollProgressChanged(Context context, float progress) {
+ if (mHomeLayout != null) {
+ mHomeLayout.setAlpha(mAlphaInterpolator.getInterpolation(progress));
+ }
+ }
+
+ @Override
+ public void onHide(Context context) {
+ if (mHomeLayout != null) {
+ mHomeLayout.setAlpha(0.0f);
+ }
+ for(ICardProvider cardProvider : mCardProviders) {
+ cardProvider.onHide(context);
+ }
+ }
+
+ @Override
+ public void onInvalidate(Context context) {
+ if (mHomeLayout != null) {
+ mHomeLayout.removeAllViews();
+ }
+ }
+
+ @Override
+ public void onRequestSearch(Context context, int mode) {
+
+ }
+
+ @Override
+ public View createCustomView(Context context) {
+ if(mHomeLayout == null) {
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mHomeLayout = (HomeLayout) inflater.inflate(R.layout.home_layout, null);
+ }
+ hideUndoBar();
+
+ mRecyclerView = (RecyclerView) mHomeLayout.findViewById(R.id.main_recycler_view);
+
+ initData();
+
+ CMHomeAdapter adapter = new CMHomeAdapter(context, mCards);
+ mLayoutManager = new LinearLayoutManager(context);
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setHasFixedSize(true);
+ mRecyclerView.setAdapter(adapter);
+
+ return mHomeLayout;
+ }
+
+ private void initData(){
+ Resources resources = mCMHomeContext.getResources();
+ TypedArray typedArray = resources.obtainTypedArray(R.array.spoof_data); ;
+ int n = typedArray.length();
+ String[][] dataArray = new String[n][];
+
+ for(int i = 0; i < n; i++){
+ int id = typedArray.getResourceId(i,0);
+ if(id > 0){
+ dataArray[i] = resources.getStringArray(id);
+ } else {
+ //something is wrong
+ }
+ }
+
+ typedArray.recycle();
+
+ mCards = new ArrayList<>();
+
+ mCards.add(new CMHomeContact(
+ dataArray[0][0],
+ dataArray[0][1],
+ dataArray[0][2],
+ dataArray[0][3]
+ ));
+
+ mCards.add(new CMHomeCalendar(
+ Long.parseLong(dataArray[1][0]),
+ Long.parseLong(dataArray[1][1]),
+ dataArray[1][2],
+ dataArray[1][3]
+ ));
+
+ for(int i = 2; i < n; i++){
+ mCards.add(new CMHomeNews(
+ dataArray[i][0],
+ dataArray[i][1],
+ dataArray[i][2],
+ Long.parseLong(dataArray[i][3]),
+ dataArray[i][4]
+ ));
+ }
+
+ Log.w("HAX", "Making data");
+ }
+
+ @Override
+ public String getName(Context context) {
+ return "HomeStub";
+ }
+
+ @Override
+ public int getNotificationFlags() {
+ return Home.FLAG_NOTIFY_ALL;
+ }
+
+ @Override
+ public int getOperationFlags() {
+ return Home.FLAG_OP_MASK;
+ }
+
+ private void hideUndoBar() {
+ View undoLayout = mHomeLayout.findViewById(R.id.list_card_undobar);
+ if (undoLayout != null) {
+ undoLayout.setVisibility(View.GONE);
+ }
+ }
+
+ public void initProvidersIfNeeded(Context context) {
+ if (mCardProviders.size() == 0) {
+ mCardProviders.add(new DashClockExtensionCardProvider(context, mHostActivityContext));
+ mCardProviders.add(new CmHomeApiCardProvider(context, mHostActivityContext,
+ mBackgroundHandler));
+
+ for (ICardProvider cardProvider : mCardProviders) {
+ cardProvider.addOnUpdateListener(mCardProviderUpdateListener);
+ }
+ }
+ }
+
+ /*
+ * Gets a list of all cards provided by each provider,
+ * and updates the UI to show them.
+ */
+ private void loadCardsFromProviders() {
+ // If cards have been initialized already, just update them
+ if(mCardArrayAdapter != null
+ && mCardArrayAdapter.getCards().size() > 0
+ && mHomeLayout != null) {
+ mBackgroundHandler.post(new RefreshAllCardsRunnable(true));
+ } else {
+ mBackgroundHandler.post(mLoadAllCardsRunnable);
+ }
+ }
+
+ /**
+ * Creates a card with a message to inform the user they have no extensions
+ * installed to publish content.
+ */
+ private Card getNoExtensionsCard(final Context context) {
+ if (mNoExtensionsCard == null) {
+ mNoExtensionsCard = new SimpleMessageCard(context);
+ mNoExtensionsCard.setTitle(context.getResources().getString(R.string.no_extensions_card_title));
+ mNoExtensionsCard.setBody(context.getResources().getString(R.string.no_extensions_card_body));
+ mNoExtensionsCard.setId(NO_EXTENSIONS_CARD_ID);
+ }
+
+ return mNoExtensionsCard;
+ }
+
+ public boolean refreshCard(String cardId) {
+ boolean cardIsNew = false;
+ if (mCardArrayAdapter != null) {
+ CmCard card = mCardArrayAdapter.getCardWithId(cardId);
+
+ // The card already exists in the list
+ if (card != null) {
+ // Allow each provider to update the card (if necessary)
+ for (ICardProvider cardProvider : mCardProviders) {
+ cardProvider.updateCard(card);
+ }
+ } else {
+ // The card is brand new, add it
+ CmCard newCard = null;
+ for (ICardProvider cardProvider : mCardProviders) {
+ newCard = cardProvider.createCardForId(cardId);
+ if (newCard != null) break;
+ }
+
+ if (newCard != null) {
+ card = newCard;
+ cardIsNew = true;
+ }
+ }
+
+ final boolean runnableCardIsNew = cardIsNew;
+ final CmCard runnableCard = card;
+ mUiThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (runnableCard != null) {
+ if (runnableCardIsNew) {
+ mCardArrayAdapter.add(runnableCard);
+ // Remove the "no cards" card, if it's there.
+ mCardArrayAdapter.remove(getNoExtensionsCard(mCMHomeContext));
+ mCardArrayAdapter.notifyDataSetChanged();
+ } else {
+ mCardArrayAdapter.updateCardViewIfVisible(runnableCard);
+ }
+ }
+ }
+ });
+ }
+ return cardIsNew;
+ }
+
+ private void removeAllCards(Context context) {
+
+ }
+
+ private void loadAllCards() {
+ final List<Card> cards = new ArrayList<Card>();
+ for (ICardProvider provider : mCardProviders) {
+ for (Card card : provider.getCards()) {
+ cards.add(card);
+ }
+ }
+
+ // If there aren't any cards, show the user a message about how to fix that!
+ if (cards.size() == 0) {
+ cards.add(getNoExtensionsCard(mCMHomeContext));
+ }
+
+ mUiThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+
+ }
+ });
+ }
+
+ /**
+ * Refresh all cards by asking the providers to update them.
+ * @param addNew If providers have new cards that have not
+ * been displayed yet, should they be added?
+ */
+ private void refreshCards(final boolean addNew) {
+ boolean noExtensionsCardExists;
+ List<CmCard> originalCards = mCardArrayAdapter.getCards();
+ int finalCardCount = 0;
+
+ final CardProviderUpdateResult updateResult =
+ new CardProviderUpdateResult(new ArrayList<CmCard>(),
+ new ArrayList<CmCard>());
+ // Allow each provider to update it's cards
+ for (ICardProvider cardProvider : mCardProviders) {
+ CardProviderUpdateResult tempResult;
+ tempResult = cardProvider.updateAndAddCards(originalCards);
+ updateResult.getCardsToAdd().addAll(tempResult.getCardsToAdd());
+ updateResult.getCardsToRemove().addAll(tempResult.getCardsToRemove());
+ }
+
+ noExtensionsCardExists = originalCards.contains(mNoExtensionsCard);
+
+ if (updateResult != null) {
+ finalCardCount += updateResult.getCardsToAdd().size();
+ finalCardCount -= updateResult.getCardsToRemove().size();
+ }
+
+ final boolean runnableNoExtensionCard = noExtensionsCardExists;
+ final int runnableFinalCardCount = finalCardCount;
+ mUiThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (updateResult != null) {
+ if (addNew) {
+ mCardArrayAdapter.addAll(updateResult.getCardsToAdd());
+ }
+ for (Card card : updateResult.getCardsToRemove()) {
+ mCardArrayAdapter.remove(card);
+ }
+ }
+
+ if (runnableNoExtensionCard && runnableFinalCardCount > 1) {
+ mCardArrayAdapter.remove(mNoExtensionsCard);
+ }
+ }
+ });
+
+ }
+
+ public class CMHomeCardArrayAdapter extends CardArrayAdapter {
+
+ public CMHomeCardArrayAdapter(Context context, List<Card> cards) {
+ super(context, cards);
+ }
+
+ public List<CmCard> getCards() {
+ List<CmCard> cardsToReturn = new ArrayList<CmCard>();
+ for(int i = 0; i < getCount(); i++) {
+ cardsToReturn.add((CmCard)getItem(i));
+ }
+ return cardsToReturn;
+ }
+
+ public CmCard getCardWithId(String id) {
+ CmCard theCard = null;
+ for(int i = 0; i < getCount(); i++) {
+ CmCard card = (CmCard) getItem(i);
+ if (card.getId().equals(id)) {
+ theCard = card;
+ break;
+ }
+ }
+ return theCard;
+ }
+
+ /**
+ * Find the CardView displaying the card that has changed
+ * and update it, if it is currently on screen. Otherwise,
+ * do nothing.
+ * @param card The card object to re-draw onscreen.
+ */
+ public void updateCardViewIfVisible(Card card) {
+ CardListView listView = getCardListView();
+ int start = listView.getFirstVisiblePosition();
+ int last = listView.getLastVisiblePosition();
+ for (int i = start; i <= last; i++) {
+ if (card == listView.getItemAtPosition(i)) {
+ View cardView = listView.getChildAt(i - start);
+ getView(i, cardView, listView);
+ break;
+ }
+ }
+ }
+ }
+
+ private class RefreshAllCardsRunnable implements Runnable {
+ private boolean mAddNew = false;
+
+ private RefreshAllCardsRunnable(boolean addNew) {
+ mAddNew = addNew;
+ }
+
+ @Override
+ public void run() {
+ refreshCards(mAddNew);
+ }
+ }
+
+ /**
+ * A DismissableManager implementation that only allows cards to be swiped to the right.
+ */
+ private class RightDismissableManager extends DefaultDismissableManager {
+ @Override
+ public SwipeDirection getSwipeDirectionAllowed() {
+ return SwipeDirection.RIGHT;
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/trebuchet/CustomHomeLauncher.java b/src/org/cyanogenmod/trebuchet/CustomHomeLauncher.java
index 7b9cff651..b5417df2e 100644
--- a/src/org/cyanogenmod/trebuchet/CustomHomeLauncher.java
+++ b/src/org/cyanogenmod/trebuchet/CustomHomeLauncher.java
@@ -16,16 +16,21 @@
package org.cyanogenmod.trebuchet;
+import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
import android.view.animation.AccelerateInterpolator;
@@ -35,38 +40,39 @@ import com.android.launcher.home.Home;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
+import org.cyanogenmod.launcher.home.HomeStub;
import org.cyanogenmod.trebuchet.home.HomeUtils;
import org.cyanogenmod.trebuchet.home.HomeWrapper;
import java.lang.Override;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
public class CustomHomeLauncher extends Launcher {
private static final String TAG = "CustomHomeLauncher";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final float MIN_PROGRESS = 0;
private static final float MAX_PROGRESS = 1;
private static class HomeAppStub {
- private final int mUid;
private final ComponentName mComponentName;
private final HomeWrapper mInstance;
- private HomeAppStub(int uid, ComponentName componentName,
- Context context, Context homeActivityContext)
+ private HomeAppStub(ComponentName componentName,
+ Context context)
throws SecurityException, ReflectiveOperationException {
super();
- mUid = uid;
mComponentName = componentName;
// Load a new instance of the Home app
ClassLoader classloader = context.getClassLoader();
Class<?> homeInterface = classloader.loadClass(Home.class.getName());
Class<?> homeClazz = classloader.loadClass(mComponentName.getClassName());
- mInstance = new HomeWrapper(context, homeInterface,
- homeClazz.newInstance(), homeActivityContext);
+ mInstance = new HomeWrapper(context, homeInterface, homeClazz.newInstance());
}
@Override
@@ -104,13 +110,13 @@ public class CustomHomeLauncher extends Launcher {
if (action.equals(Intent.ACTION_PACKAGE_CHANGED) ||
action.equals(Intent.ACTION_PACKAGE_REPLACED) ||
action.equals(Intent.ACTION_PACKAGE_RESTARTED)) {
- if (mCurrentHomeApp != null && intent.getIntExtra(Intent.EXTRA_UID, -1)
+ /*if (mCurrentHomeApp != null && intent.getIntExtra(Intent.EXTRA_UID, -1)
== mCurrentHomeApp.mUid) {
// The current Home app has changed or restarted. Invalidate the current
// one to be sure we will get all the new changes (if any)
if (DEBUG) Log.d(TAG, "Home package has changed. Invalidate layout.");
invalidate = true;
- }
+ }*/
}
obtainCurrentHomeAppStubLocked(invalidate);
}
@@ -260,8 +266,8 @@ public class CustomHomeLauncher extends Launcher {
if (DEBUG) Log.d(TAG, "obtainCurrentHomeAppStubLocked called (" + invalidate + ")");
SparseArray<ComponentName> packages = HomeUtils.getInstalledHomePackages(this);
- if (!invalidate && mCurrentHomeApp != null &&
- packages.get(mCurrentHomeApp.mUid) != null) {
+ if (!invalidate && mCurrentHomeApp != null/* &&
+ packages.get(mCurrentHomeApp.mUid) != null*/) {
// We still have a valid Home app
return;
}
@@ -269,7 +275,7 @@ public class CustomHomeLauncher extends Launcher {
// We don't have a valid Home app, so we need to destroy the current the custom content
destroyHomeStub();
- // Return the default valid home app
+ /*// Return the default valid home app
int size = packages.size();
for (int i = 0; i < size; i++) {
int key = packages.keyAt(i);
@@ -301,6 +307,29 @@ public class CustomHomeLauncher extends Launcher {
if (mCurrentHomeApp != null) {
mCurrentHomeApp.mInstance.onStart();
}
+ }*/
+
+ ComponentName pkg = new ComponentName(getPackageName(), HomeStub.class.getName());
+ try {
+ mCurrentHomeApp = new HomeAppStub(pkg, this);
+ } catch (ReflectiveOperationException e) {
+ if (!DEBUG) {
+ Log.w(TAG, "Cannot instantiate home package: " + pkg + ". Ignored.");
+ } else {
+ Log.w(TAG, "Cannot instantiate home package: " + pkg +
+ ". Ignored.", e);
+ }
+ } catch (SecurityException ex) {
+ if (!DEBUG) {
+ Log.w(TAG, "Home package is insecure: " + pkg + ". Ignored.");
+ } else {
+ Log.w(TAG, "Home package is insecure: " + pkg + ". Ignored.", ex);
+ }
+ }
+
+ // Notify home app that is going to be used
+ if (mCurrentHomeApp != null) {
+ mCurrentHomeApp.mInstance.onStart();
}
// Don't have a valid package. Anyway notify the launcher that custom content has changed
diff --git a/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java b/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java
index 1c7dfda66..3b1dff033 100644
--- a/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java
+++ b/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java
@@ -56,11 +56,10 @@ public class HomeWrapper {
private final int mOperationFlags;
public HomeWrapper(Context context, Class<?> cls,
- Object instance,
- Context hostActivityContext) throws SecurityException {
+ Object instance) throws SecurityException {
super();
mContext = context;
- mHostActivityContext = hostActivityContext;
+ mHostActivityContext = context;
mClass = cls;
mInstance = instance;
cachedMethods = new SparseArray<Method>(M_LAST_ID);