diff options
author | Raj Yengisetty <rajesh@cyngn.com> | 2015-07-27 14:52:21 -0700 |
---|---|---|
committer | Raj Yengisetty <rajesh@cyngn.com> | 2015-07-27 14:52:21 -0700 |
commit | 8847eb7a1ddb0b19c0c79de6d867444a68654874 (patch) | |
tree | 26b692977e92d5821165a295e08d8d081d05f18d /src/org/cyanogenmod | |
parent | 8d355bd41dcb91c7bb1da5a31755381b49fa663e (diff) | |
download | android_packages_apps_Trebuchet-staging/cmhome.tar.gz android_packages_apps_Trebuchet-staging/cmhome.tar.bz2 android_packages_apps_Trebuchet-staging/cmhome.zip |
Move CMHome into Trebuchetstaging/cmhome
Change-Id: I781d10319d183cfb6acca5926ebf64920caf570e
Diffstat (limited to 'src/org/cyanogenmod')
18 files changed, 2845 insertions, 15 deletions
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); |