diff options
24 files changed, 981 insertions, 226 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 34e25b8b4..b26dfcf91 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -32,6 +32,10 @@ android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="signatureOrSystem" /> <permission + android:name="com.android.launcher.home.permissions.HOME_APP" + android:permissionGroup="android.permission-group.SYSTEM_TOOLS" + android:protectionLevel="dangerous" /> + <permission android:name="com.android.launcher3.permission.READ_SETTINGS" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="dangerous" @@ -73,7 +77,7 @@ android:largeHeap="@bool/config_largeHeap" android:supportsRtl="true"> <activity - android:name="com.android.launcher3.Launcher" + android:name="org.cyanogenmod.trebuchet.TrebuchetLauncher" android:launchMode="singleTask" android:clearTaskOnLaunch="true" android:stateNotNeeded="true" diff --git a/res/drawable-hdpi/search_bg.9.png b/res/drawable-hdpi/search_bg.9.png Binary files differnew file mode 100644 index 000000000..798127b50 --- /dev/null +++ b/res/drawable-hdpi/search_bg.9.png diff --git a/res/drawable-mdpi/search_bg.9.png b/res/drawable-mdpi/search_bg.9.png Binary files differnew file mode 100644 index 000000000..3b259d4fb --- /dev/null +++ b/res/drawable-mdpi/search_bg.9.png diff --git a/res/drawable-xhdpi/search_bg.9.png b/res/drawable-xhdpi/search_bg.9.png Binary files differnew file mode 100644 index 000000000..947fd4de6 --- /dev/null +++ b/res/drawable-xhdpi/search_bg.9.png diff --git a/res/drawable-xxhdpi/search_bg.9.png b/res/drawable-xxhdpi/search_bg.9.png Binary files differnew file mode 100644 index 000000000..6626cd99b --- /dev/null +++ b/res/drawable-xxhdpi/search_bg.9.png diff --git a/res/layout-port/qsb.xml b/res/layout-port/qsb.xml index 4c9963dfb..f7a1c8773 100644 --- a/res/layout-port/qsb.xml +++ b/res/layout-port/qsb.xml @@ -18,7 +18,7 @@ xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/search_frame"> + android:background="@drawable/search_bg"> <!-- Global search icon --> <com.android.launcher3.HolographicLinearLayout style="@style/SearchButton.WithPaddingStart" diff --git a/res/layout-sw720dp/qsb.xml b/res/layout-sw720dp/qsb.xml index e993f7847..399666ab0 100644 --- a/res/layout-sw720dp/qsb.xml +++ b/res/layout-sw720dp/qsb.xml @@ -19,7 +19,7 @@ style="@style/SearchDropTargetBar" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/search_frame"> + android:background="@drawable/search_bg"> <!-- Global search icon --> <com.android.launcher3.HolographicLinearLayout style="@style/SearchButton.WithPaddingStart" diff --git a/res/layout/list_item_checkable.xml b/res/layout/list_item_checkable.xml deleted file mode 100644 index 86d6b1a4c..000000000 --- a/res/layout/list_item_checkable.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2010 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeightSmall" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:attr/textColorAlertDialogListItem" - android:gravity="center_vertical" - android:paddingStart="16dip" - android:paddingEnd="16dip" - android:checkMark="?android:attr/listChoiceIndicatorSingle" - android:ellipsize="marquee" /> diff --git a/res/values/cm_integers.xml b/res/values/cm_integers.xml new file mode 100644 index 000000000..f3b0464ef --- /dev/null +++ b/res/values/cm_integers.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <integer name="qsb_initial_alpha_state">55</integer> + <integer name="qsb_end_alpha_state">220</integer> + <integer name="qsb_buttons_end_colorfilter">120</integer> +</resources> diff --git a/res/values/preferences_defaults.xml b/res/values/preferences_defaults.xml index b6ae9a6e6..64d904a0f 100644 --- a/res/values/preferences_defaults.xml +++ b/res/values/preferences_defaults.xml @@ -13,6 +13,4 @@ <bool name="preferences_interface_drawer_remove_hidden_apps_widgets_default">true</bool> <bool name="preferences_interface_drawer_hide_icon_labels_default">false</bool> <bool name="preferences_interface_general_icons_large_default">@bool/config_largeIcons</bool> - <string name="preferences_interface_general_icons_text_font_family_default">sans-serif-condensed</string> - <integer name="preferences_interface_general_icons_text_font_style_default">0</integer> </resources> diff --git a/src/com/android/launcher/home/Home.java b/src/com/android/launcher/home/Home.java new file mode 100644 index 000000000..5dce71e86 --- /dev/null +++ b/src/com/android/launcher/home/Home.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher.home; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +/** + * The generic contract that should supports a <code>Home</code> app to could + * be invoked and registered by an Android launcher.<br/> + * <br/> + * This interface contains the version 1 of the <code>Home Host App</code> protocol.<br/> + * <br/> + * <br/> + * A <code>Home</code> app should: + * <ul> + * <li> + * should have at least a constructor with no arguments + * </li> + * <li> + * declares inside its manifest a <code>com.android.launcher.home</code> metadata with the + * full qualified that contains this interface<br/> + * <pre> + * <meta-data android:name="com.android.launcher.home" value="org.cyanogenmod.launcher.home.HomeStub"/> + * </pre> + * </li> + * <li> + * define the "com.android.launcher.home.permissions.HOME_APP" permission<br/> + * <pre> + * <uses-permission android:name="com.android.launcher.home.permissions.HOME_APP"/> + * </pre> + * </li> + * <li> + * implements the contract defined by this protocol. + * </li> + * </ul> + * <br/> + * Implementors classes of this protocol should be aware that all the {@link Context} references + * passed to this class owns to the host launcher app. This means that you cannot access + * to settings defined by the <code>Home</code> app inside its shared context. + */ +public interface Home { + + /** + * A SHA-1 hash of all declared method of this interface. Home apps should compute as:<br/> + * <br/> + * <pre> + * for method in Home.class.getDeclaredMehod + * sha1.update method.toString.bytes + * </pre><br/> + * DO NOT MODIFY! + */ + public static final String SIGNATURE = "5/A6Mxkz8gHHzzVf4qZR+hiSOAw="; + + /** + * Defines the name of the metadata used to declared the full qualified Home stub class + * that implements this protocol. + */ + public static final String METADATA_HOME_STUB = "com.android.launcher.home"; + + /** + * Defines the name of the permission that the Home app should explicitly declare. + */ + public static final String PERMISSION_HOME_APP = "com.android.launcher.home.permissions.HOME_APP"; + + // Notification flags + public static final int FLAG_NOTIFY_MASK = 0x0000; + public static final int FLAG_NOTIFY_ON_RESUME = FLAG_NOTIFY_MASK + 0x0002; + public static final int FLAG_NOTIFY_ON_PAUSE = FLAG_NOTIFY_MASK + 0x0004; + public static final int FLAG_NOTIFY_ON_SHOW = FLAG_NOTIFY_MASK + 0x0008; + public static final int FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED = FLAG_NOTIFY_MASK + 0x0010; + public static final int FLAG_NOTIFY_ON_HIDE = FLAG_NOTIFY_MASK + 0x0020; + public static final int FLAG_NOTIFY_ALL = FLAG_NOTIFY_ON_RESUME | FLAG_NOTIFY_ON_PAUSE | + FLAG_NOTIFY_ON_SHOW | FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED | FLAG_NOTIFY_ON_HIDE; + + // Operation support flags + public static final int FLAG_OP_MASK = 0x1000; + public static final int FLAG_OP_CUSTOM_SEARCH = FLAG_OP_MASK + 0x0002; + public static final int FLAG_OP_ALL = FLAG_OP_CUSTOM_SEARCH; + + // Search modes + public static final int MODE_SEARCH_TEXT = 0x0000; + public static final int MODE_SEARCH_VOICE = 0x0001; + + /** + * Invoked the first time the <code>Home</code> app is created.<br/> + * This method should be used by implementors classes of this protocol to load the needed + * resources. + * @param context the current {@link Context} of the host launcher. + */ + void onStart(Context context); + + /** + * Invoked when the <code>Home</code> app should be destroy.<br/> + * This method should be used by implementors classes of this protocol to unload all unneeded + * resources. + * @param context the current {@link Context} of the host launcher. + */ + void onDestroy(Context context); + + /** + * Invoked when the host launcher enters in resume mode. + * @param context the current {@link Context} of the host launcher. + */ + void onResume(Context context); + + /** + * Invoked when the host launcher enters in pause mode. + * @param context the current {@link Context} of the host launcher. + */ + void onPause(Context context); + + /** + * Invoked when the custom content page is totally displayed. + * @param context the current {@link Context} of the host launcher. + */ + void onShow(Context context); + + /** + * Invoked when the custom content page is scrolled. + * @param context the current {@link Context} of the host launcher. + * @param progress the current scroll progress. + */ + void onScrollProgressChanged(Context context, float progress); + + /** + * Invoked when the custom content page is totally hidden. + * @param context the current {@link Context} of the host launcher. + */ + void onHide(Context context); + + /** + * Invoked by the host launcher to request an invalidation of the ui elements and data used by + * the <code>Home</code> implementation class. + * @param context the current {@link Context} of the host launcher. + */ + void onInvalidate(Context context); + + /** + * Invoked when the host launcher request enter in search mode. + * @param context the current {@link Context} of the host launcher. + * @param mode the requested search mode. Must be one of: + * <ul> + * <li>{@link #MODE_SEARCH_TEXT}: Textual mode</li> + * <li>{@link #MODE_SEARCH_VOICE}: Voice mode</li> + * </ul> + */ + void onRequestSearch(Context context, int mode); + + /** + * Returns an instance of a {@link View} that holds the custom content to be displayed + * by this <code>Home</code> app. + * @param context the current {@link Context} of the host launcher. + * @return View The custom content view that will be enclosed inside a + * <code>com.android.launcher3.Launcher.QSBScroller</code>.<br/> + * Be aware the the height layout of the returned should be defined as + * {link {@link LayoutParams#WRAP_CONTENT}, so the view could be scrolled inside the + * custom content page. + */ + View createCustomView(Context context); + + /** + * Returns the name of the Home app (LIMIT: 30 characters). + * @param context the current {@link Context} of the host launcher. + */ + String getName(Context context); + + /** + * Implementations should return the combination of notification flags that want to listen to. + * @see #FLAG_NOTIFY_ON_RESUME + * @see #FLAG_NOTIFY_ON_PAUSE + * @see #FLAG_NOTIFY_ON_SHOW + * @see #FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED + * @see #FLAG_NOTIFY_ON_HIDE + * @see #FLAG_NOTIFY_ALL + */ + int getNotificationFlags(); + + /** + * Implementations should return the combination of operation flags that want they want + * to support to. + * @see #FLAG_OP_CUSTOM_SEARCH + * @see #FLAG_OP_ALL + */ + int getOperationFlags(); +} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java index 6015de624..f2c08a3e1 100644 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ b/src/com/android/launcher3/AppsCustomizePagedView.java @@ -1176,7 +1176,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen icon.setOnLongClickListener(this); icon.setOnTouchListener(this); icon.setOnKeyListener(this); - Utilities.applyTypeface(icon); int index = i - startIndex; int x = index % mCellCountX; diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 779d26955..9d7907c43 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -664,7 +664,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList textView.setTag(item); textView.setTextColor(getResources().getColor(R.color.folder_items_text_color)); textView.setShadowsEnabled(false); - Utilities.applyTypeface(textView); textView.setGlowColor(getResources().getColor(R.color.folder_items_glow_color)); textView.setOnClickListener(this); diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index 51517548c..464b35da9 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -149,7 +149,6 @@ public class FolderIcon extends FrameLayout implements FolderListener { icon.setClipToPadding(false); icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); icon.mFolderName.setText(folderInfo.title); - Utilities.applyTypeface(icon.mFolderName); icon.mFolderName.setCompoundDrawablePadding(0); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; diff --git a/src/com/android/launcher3/GelIntegrationHelper.java b/src/com/android/launcher3/GelIntegrationHelper.java index abda537a2..6dd9c0e32 100644 --- a/src/com/android/launcher3/GelIntegrationHelper.java +++ b/src/com/android/launcher3/GelIntegrationHelper.java @@ -63,10 +63,10 @@ public class GelIntegrationHelper { if(GEL_ACTIVITY.equals(topActivityClassName) && GEL_PACKAGE_NAME.equals(topActivityPackageName)) { Intent homeIntent = new Intent(Intent.ACTION_MAIN); - homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); homeIntent.addCategory(Intent.CATEGORY_HOME); launcherActivity.startActivity(homeIntent); - launcherActivity.overridePendingTransition(0, R.anim.exit_out_left); + launcherActivity.overridePendingTransition(0, 0); dropEventsUntilLift(); } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 8fa5cfdd6..2947eda0f 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -56,6 +56,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -99,6 +100,7 @@ import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import com.android.launcher.home.Home; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.PagedView.TransitionEffect; import com.android.launcher3.settings.SettingsProvider; @@ -289,6 +291,9 @@ public class Launcher extends Activity private AppsCustomizePagedView mAppsCustomizeContent; private boolean mAutoAdvanceRunning = false; private View mQsb; + private View mQsbBar; + private ImageView mQsbBarSearch; + private ImageView mQsbBarVoice; private Bundle mSavedState; // We set the state in both onCreate and then onNewIntent in some cases, which causes both @@ -578,6 +583,10 @@ public class Launcher extends Activity } /** To be overridden by subclasses to hint to Launcher that we have custom content */ + protected boolean hasCustomSearchSupport() { + return false; + } + protected boolean hasCustomContentToLeft() { return isGelIntegrationSupported() && isGelIntegrationEnabled(); } @@ -652,6 +661,15 @@ public class Launcher extends Activity } } + /** To be overriden by subclasses to hint to Launcher that we have custom content and + * support {@link #hasCustomSearchSupport()} + * @see com.android.launcher.home.Home#MODE_SEARCH_TEXT + * @see com.android.launcher.home.Home#MODE_SEARCH_VOICE + * */ + protected void requestSearch(int mode) { + // To be implemented + } + private void updateGlobalIcons() { boolean searchVisible = false; boolean voiceVisible = false; @@ -1619,7 +1637,6 @@ public class Launcher extends Activity favorite.setCompoundDrawables(null, d, null, null); favorite.setOnTouchListener(getHapticFeedbackTouchListener()); } - Utilities.applyTypeface(favorite); return favorite; } @@ -2740,8 +2757,12 @@ public class Launcher extends Activity * @param v The view that was clicked. */ public void onClickSearchButton(View v) { - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + if (hasCustomSearchSupport()) { + requestSearch(Home.MODE_SEARCH_TEXT); + return; + } + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); onSearchRequested(); } @@ -2751,8 +2772,12 @@ public class Launcher extends Activity * @param v The view that was clicked. */ public void onClickVoiceButton(View v) { - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + if (hasCustomSearchSupport()) { + requestSearch(Home.MODE_SEARCH_VOICE); + return; + } + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); startVoice(); } @@ -3826,6 +3851,20 @@ public class Launcher extends Activity return mQsb; } + public ImageView getQsbBarSearchButton() { + if (mQsbBarSearch == null && getQsbBar() != null) { + return (ImageView) getQsbBar().findViewById(R.id.search_button); + } + return mQsbBarSearch; + } + + public ImageView getQsbBarVoiceButton() { + if (mQsbBarVoice == null && getQsbBar() != null) { + return (ImageView) getQsbBar().findViewById(R.id.voice_button); + } + return mQsbBarVoice; + } + protected boolean updateGlobalSearchIcon() { final View searchButtonContainer = findViewById(R.id.search_button_container); final ImageView searchButton = (ImageView) findViewById(R.id.search_button); diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 50ff612e9..11e18b186 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -24,8 +24,6 @@ import android.database.ContentObserver; import android.os.Handler; import android.util.Log; -import com.android.launcher3.settings.SettingsProvider; - import java.lang.ref.WeakReference; public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { @@ -116,18 +114,6 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { ContentResolver resolver = sContext.getContentResolver(); resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, mFavoritesObserver); - - // Generate default typeface - String fontFamily = SettingsProvider.getString(sContext, - SettingsProvider.SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_FAMILY, - R.string.preferences_interface_general_icons_text_font_family_default); - - // TODO: Implement font styles - int fontStyle = SettingsProvider.getInt(sContext, - SettingsProvider.SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_STYLE, - R.integer.preferences_interface_general_icons_text_font_style_default); - - Utilities.generateTypeface(fontFamily, fontStyle); } public void recreateWidgetPreviewDb() { diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index fc6496d00..cbc978585 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -21,14 +21,21 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.graphics.*; +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; -import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; @@ -51,8 +58,6 @@ public final class Utilities { private static final Rect sOldBounds = new Rect(); private static final Canvas sCanvas = new Canvas(); - private static Typeface sTypeface; - static { sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG)); @@ -213,27 +218,6 @@ public final class Utilities { } /** - * Generates the default icon typeface for use in icons. - * - * @param familyName May be null. The name of the font family. - * @param style The style (normal, bold, italic) of the typeface. e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC - */ - static void generateTypeface(String familyName, int style) { - sTypeface = Typeface.create(familyName, style); - } - - /** - * Applies the default icon typeface to a textview. - * - * @param textView View to apply typeface to. - */ - static void applyTypeface(TextView textView) { - if (sTypeface != null) { - textView.setTypeface(sTypeface); - } - } - - /** * Given a coordinate relative to the descendant, find the coordinate in a parent view's * coordinates. * diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 008dc1527..c67ca9395 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -1255,7 +1255,8 @@ public class Workspace extends SmoothPagedView super.notifyPageSwitchListener(); Launcher.setScreen(getNextPage()); - if (hasCustomContent() && getNextPage() == 0 && !mCustomContentShowing) { + int ccIndex = getPageIndexForScreenId(CUSTOM_CONTENT_SCREEN_ID); + if (hasCustomContent() && getNextPage() == ccIndex && !mCustomContentShowing) { mCustomContentShowing = true; if (mCustomContentCallbacks != null) { @@ -1263,7 +1264,7 @@ public class Workspace extends SmoothPagedView mCustomContentShowTime = System.currentTimeMillis(); mLauncher.updateVoiceButtonProxyVisible(false); } - } else if (hasCustomContent() && mCustomContentShowing) { + } else if (hasCustomContent() && getNextPage() != ccIndex && mCustomContentShowing) { mCustomContentShowing = false; if (mCustomContentCallbacks != null) { mCustomContentCallbacks.onHide(); @@ -1826,12 +1827,17 @@ public class Workspace extends SmoothPagedView // them mLastSetWallpaperOffsetSteps = 0f; + moveAwayFromCustomContentIfRequired(); + } + + public void moveAwayFromCustomContentIfRequired() { // Never resume to the custom page if GEL integration is enabled. int customPageIndex = getPageIndexForScreenId(CUSTOM_CONTENT_SCREEN_ID); // mCustomContentShowing can be lost if the Activity is recreated, // So make sure it is set to the right value. mCustomContentShowing = mCustomContentShowing - || (customPageIndex == getCurrentPage() && hasCustomContent()); + || (customPageIndex == getCurrentPage() + && hasCustomContent()); if (mCustomContentShowing && mLauncher.isGelIntegrationEnabled()) { moveToScreen((customPageIndex + 1), true); } @@ -1844,6 +1850,8 @@ public class Workspace extends SmoothPagedView mWallpaperOffset.jumpToFinal(); } super.onLayout(changed, left, top, right, bottom); + + moveAwayFromCustomContentIfRequired(); } @Override @@ -4187,7 +4195,12 @@ public class Workspace extends SmoothPagedView } public int getCurrentPageOffsetFromCustomContent() { - return getNextPage() - numCustomPages(); + int numCustomPages = numCustomPages(); + // Special case where the Gel Integration page must be counted below + if(mLauncher.isGelIntegrationEnabled() && mLauncher.isGelIntegrationSupported()) { + numCustomPages += 1; + } + return getNextPage() - numCustomPages; } /** diff --git a/src/com/android/launcher3/settings/FontStylePreference.java b/src/com/android/launcher3/settings/FontStylePreference.java deleted file mode 100644 index 50df4b280..000000000 --- a/src/com/android/launcher3/settings/FontStylePreference.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.android.launcher3.settings; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.Typeface; -import android.preference.ListPreference; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.*; -import com.android.launcher3.R; - -import java.util.Arrays; -import java.util.List; - -public class FontStylePreference extends ListPreference { - private String mValue; - public int mClickedDialogEntryIndex; - private boolean mValueSet; - - public FontStylePreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - if (getEntries() == null || getEntryValues() == null) { - throw new IllegalStateException( - "ListPreference requires an entries array and an entryValues array."); - } - - mClickedDialogEntryIndex = getValueIndex(); - builder.setSingleChoiceItems(new FontStyleAdapter(getContext()), mClickedDialogEntryIndex, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - mClickedDialogEntryIndex = which; - - /* - * Clicking on an item simulates the positive button - * click, and dismisses the dialog. - */ - FontStylePreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); - dialog.dismiss(); - } - }); - - /* - * The typical interaction for list-based dialogs is to have - * click-on-an-item dismiss the dialog instead of the user having to - * press 'Ok'. - */ - builder.setPositiveButton(null, null); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { - String value = getEntryValues()[mClickedDialogEntryIndex].toString(); - if (callChangeListener(value)) { - setValue(value); - } - } - } - - @Override - protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { - setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue); - } - - /** - * Sets the value of the key. This should be one of the entries in - * {@link #getEntryValues()}. - * - * @param value The value to set for the key. - */ - @Override - public void setValue(String value) { - // Always persist/notify the first time. - final boolean changed = !TextUtils.equals(mValue, value); - if (changed || !mValueSet) { - mValue = value; - mValueSet = true; - persistString(value); - if (changed) { - notifyChanged(); - } - } - } - - /** - * Returns the value of the key. This should be one of the entries in - * {@link #getEntryValues()}. - * - * @return The value of the key. - */ - @Override - public String getValue() { - return mValue; - } - - private int getValueIndex() { - return findIndexOfValue(mValue); - } - - private class FontStyleAdapter extends ArrayAdapter<CharSequence> { - private LayoutInflater mInflater; - private List<CharSequence> mEntries; - private List<CharSequence> mValues; - - public FontStyleAdapter(Context context) { - super(context, -1, getEntryValues()); - - mInflater = LayoutInflater.from(context); - mEntries = Arrays.asList(getEntries()); - mValues = Arrays.asList(getEntryValues()); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - CheckedTextView textView; - - if (convertView == null) { - textView = (CheckedTextView) mInflater.inflate(R.layout.list_item_checkable, parent, false); - } else { - textView = (CheckedTextView) convertView; - } - - if (textView != null) { - textView.setText(mEntries.get(position)); - textView.setTag(mValues.get(position)); - textView.setTypeface(Typeface.create((String) mValues.get(position), Typeface.NORMAL)); - } - - return textView; - } - } -} diff --git a/src/com/android/launcher3/settings/SettingsProvider.java b/src/com/android/launcher3/settings/SettingsProvider.java index c827ae478..d7782b2e6 100644 --- a/src/com/android/launcher3/settings/SettingsProvider.java +++ b/src/com/android/launcher3/settings/SettingsProvider.java @@ -38,10 +38,10 @@ public final class SettingsProvider { public static final String SETTINGS_UI_DRAWER_REMOVE_HIDDEN_APPS_WIDGETS = "ui_drawer_remove_hidden_apps_widgets"; public static final String SETTINGS_UI_DRAWER_HIDE_ICON_LABELS = "ui_drawer_hide_icon_labels"; public static final String SETTINGS_UI_GENERAL_ICONS_LARGE = "ui_general_icons_large"; - public static final String SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_FAMILY = "ui_general_icons_text_font"; - public static final String SETTINGS_UI_GENERAL_ICONS_TEXT_FONT_STYLE = "ui_general_icons_text_font_style"; public static final String SETTINGS_UI_DRAWER_SORT_MODE = "ui_drawer_sort_mode"; + public static final String SETTINGS_HOME_LAST_APP = "home_last_app"; + public static SharedPreferences get(Context context) { return context.getSharedPreferences(SETTINGS_KEY, Context.MODE_MULTI_PROCESS); } diff --git a/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java b/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java new file mode 100644 index 000000000..ec91e10d7 --- /dev/null +++ b/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java @@ -0,0 +1,332 @@ +/* + * 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.trebuchet; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseArray; +import android.view.animation.AccelerateInterpolator; +import android.widget.ImageView; + +import com.android.launcher.home.Home; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; + +import org.cyanogenmod.trebuchet.home.HomeUtils; +import org.cyanogenmod.trebuchet.home.HomeWrapper; + +import java.lang.Override; + +public class TrebuchetLauncher extends Launcher { + + private static final String TAG = "TrebuchetLauncher"; + + private static final boolean DEBUG = false; + 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) + 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()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mComponentName == null) ? 0 : mComponentName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + HomeAppStub other = (HomeAppStub) obj; + if (mComponentName == null) { + if (other.mComponentName != null) + return false; + } else if (!mComponentName.equals(other.mComponentName)) + return false; + return true; + } + } + + private BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Obtain the current instance or a new one if the current instance not exists + boolean invalidate = false; + String action = intent.getAction(); + 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) + == 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); + } + }; + + private CustomContentCallbacks mCustomContentCallbacks = new CustomContentCallbacks() { + @Override + public void onShow() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onShow(); + } + } + + @Override + public void onScrollProgressChanged(float progress) { + updateQsbBarColorState(progress); + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onScrollProgressChanged(progress); + } + } + + @Override + public void onHide() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onHide(); + } + } + }; + + private HomeAppStub mCurrentHomeApp; + private AccelerateInterpolator mQSBAlphaInterpolator; + + private QSBScroller mQsbScroller; + private int mQsbInitialAlphaState; + private int mQsbEndAlphaState; + private int mQsbButtonsEndColorFilter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mQSBAlphaInterpolator = new AccelerateInterpolator(); + + // Set QsbBar color state + final Resources res = getResources(); + mQsbInitialAlphaState = res.getInteger(R.integer.qsb_initial_alpha_state); + mQsbEndAlphaState = res.getInteger(R.integer.qsb_end_alpha_state); + mQsbButtonsEndColorFilter = res.getInteger(R.integer.qsb_buttons_end_colorfilter); + updateQsbBarColorState(MIN_PROGRESS); + + // Obtain the user-defined Home app or a valid one + obtainCurrentHomeAppStubLocked(true); + + // Register this class to listen for new/deleted packages + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addDataScheme("package"); + registerReceiver(mPackageReceiver, filter); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // Unregister services + unregisterReceiver(mPackageReceiver); + } + + @Override + protected void onResume() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onResume(); + } + super.onResume(); + } + + @Override + protected void onPause() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onPause(); + } + super.onPause(); + } + + @Override + protected boolean hasCustomContentToLeft() { + return mCurrentHomeApp != null && super.hasCustomContentToLeft(); + } + + @Override + protected void invalidateHasCustomContentToLeft() { + invalidateHomeStub(); + super.invalidateHasCustomContentToLeft(); + } + + @Override + protected void populateCustomContentContainer() { + if (mCurrentHomeApp != null) { + mQsbScroller = addToCustomContentPage(mCurrentHomeApp.mInstance.createCustomView(), + mCustomContentCallbacks, mCurrentHomeApp.mInstance.getName()); + mQsbScroller.setScrollY(200); + } + } + + @Override + protected boolean hasCustomSearchSupport() { + return hasCustomContentToLeft() && mCurrentHomeApp.mInstance.isOperationSupported( + Home.FLAG_OP_CUSTOM_SEARCH); + } + + @Override + protected void requestSearch(int mode) { + if (!hasCustomSearchSupport()) { + return; + } + mCurrentHomeApp.mInstance.onRequestSearch(mode); + } + + private synchronized void obtainCurrentHomeAppStubLocked(boolean invalidate) { + if (DEBUG) Log.d(TAG, "obtainCurrentHomeAppStubLocked called (" + invalidate + ")"); + + SparseArray<ComponentName> packages = HomeUtils.getInstalledHomePackages(this); + if (!invalidate && mCurrentHomeApp != null && + packages.get(mCurrentHomeApp.mUid) != null) { + // We still have a valid Home app + return; + } + + // 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 + int size = packages.size(); + for (int i = 0; i < size; i++) { + int key = packages.keyAt(i); + ComponentName pkg = packages.get(key); + String qualifiedPkg = pkg.toShortString(); + Context ctx = HomeUtils.createNewHomePackageContext(this, pkg); + if (ctx == null) { + // We failed to create a valid context. Will try with the next package + continue; + } + try { + mCurrentHomeApp = new HomeAppStub(key, pkg, ctx); + } catch (ReflectiveOperationException ex) { + if (!DEBUG) { + Log.w(TAG, "Cannot instantiate home package: " + qualifiedPkg + ". Ignored."); + } else { + Log.w(TAG, "Cannot instantiate home package: " + qualifiedPkg + + ". Ignored.", ex); + } + } catch (SecurityException ex) { + if (!DEBUG) { + Log.w(TAG, "Home package is insecure: " + qualifiedPkg + ". Ignored."); + } else { + Log.w(TAG, "Home package is insecure: " + qualifiedPkg + ". 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 + invalidateHasCustomContentToLeft(); + } + + private void invalidateHomeStub() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onInvalidate(); + if (DEBUG) Log.d(TAG, "Home package " + mCurrentHomeApp.mComponentName.toShortString() + + " was invalidated."); + } + } + + private void destroyHomeStub() { + if (mCurrentHomeApp != null) { + mCurrentHomeApp.mInstance.onInvalidate(); + mCurrentHomeApp.mInstance.onDestroy(); + if (DEBUG) Log.d(TAG, "Home package " + mCurrentHomeApp.mComponentName.toShortString() + + " was destroyed."); + } + mQsbScroller = null; + mCurrentHomeApp = null; + } + + private void updateQsbBarColorState(float progress) { + if (getQsbBar() != null) { + float interpolation = mQSBAlphaInterpolator.getInterpolation(progress); + + // Background alpha + int alphaInterpolation = (int)(mQsbInitialAlphaState + + (interpolation * (mQsbEndAlphaState - mQsbInitialAlphaState))); + Drawable background = getQsbBar().getBackground(); + if (background != null) { + background.setAlpha(alphaInterpolation); + } + + // Buttons color filter + int colorInterpolation = (int)(255 - (interpolation * mQsbButtonsEndColorFilter)); + int color = Color.rgb(colorInterpolation, colorInterpolation,colorInterpolation); + ImageView voiceButton = getQsbBarVoiceButton(); + if (voiceButton != null) { + if (progress > 0) { + voiceButton.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + ImageView searchButton = getQsbBarSearchButton(); + if (searchButton != null) { + if (progress > 0) { + searchButton.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + } + } +} diff --git a/src/org/cyanogenmod/trebuchet/home/HomeUtils.java b/src/org/cyanogenmod/trebuchet/home/HomeUtils.java new file mode 100644 index 000000000..63bf5ff60 --- /dev/null +++ b/src/org/cyanogenmod/trebuchet/home/HomeUtils.java @@ -0,0 +1,112 @@ +/* + * 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.trebuchet.home; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseArray; + +import com.android.launcher.home.Home; + +import java.util.List; + +public class HomeUtils { + + private static final String TAG = "HomeUtils"; + + // FIXME For now for security reason we will only support known Home apps + private static final String[] WELL_KNOWN_HOME_APP_PKGS = + { + "org.cyanogenmod.launcher.home" + }; + + + public static final SparseArray<ComponentName> getInstalledHomePackages(Context context) { + // A Home app should: + // - declare the use of Home.PERMISSION_HOME_APP permission. + // - define the home stub class through the Home.METADATA_HOME_STUB metadata + SparseArray<ComponentName> installedHomePackages = new SparseArray<ComponentName>(); + + PackageManager packageManager = context.getPackageManager(); + + List<PackageInfo> installedPackages = packageManager.getInstalledPackages( + PackageManager.GET_PERMISSIONS); + for (PackageInfo pkg : installedPackages) { + boolean hasHomeAppPermission = false; + if (pkg.requestedPermissions != null) { + for (String perm : pkg.requestedPermissions) { + if (perm.equals(Home.PERMISSION_HOME_APP)) { + hasHomeAppPermission = true; + break; + } + } + } + if (hasHomeAppPermission) { + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(pkg.packageName, + PackageManager.GET_META_DATA); + Bundle metadata = appInfo.metaData; + if (metadata != null && metadata.containsKey(Home.METADATA_HOME_STUB)) { + String homeStub = metadata.getString(Home.METADATA_HOME_STUB); + installedHomePackages.put(appInfo.uid, + new ComponentName(pkg.packageName, homeStub)); + } + } catch (NameNotFoundException ex) { + // Ignored. The package doesn't exists ¿? + } + } + } + + // FIXME For now we only support known Home apps. Remove this checks when + // Trebuchet allows Home apps through the full Home Host Protocol + if (installedHomePackages.size() > 0) { + for (String pkg : WELL_KNOWN_HOME_APP_PKGS) { + int i = installedHomePackages.size() - 1; + boolean isWellKnownPkg = false; + for (; i >= 0; i--) { + int key = installedHomePackages.keyAt(i); + if (installedHomePackages.get(key).getPackageName().equals(pkg)) { + isWellKnownPkg = true; + break; + } + } + if (!isWellKnownPkg) { + installedHomePackages.removeAt(i); + } + } + } + + return installedHomePackages; + } + + public static Context createNewHomePackageContext(Context ctx, ComponentName pkg) { + // Create a new context package for the current user + try { + return ctx.createPackageContext(pkg.getPackageName(), + Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); + } catch (NameNotFoundException ex) { + Log.e(TAG, "Failed to load a home package context. Package not found.", ex); + } + return null; + } +} diff --git a/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java b/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java new file mode 100644 index 000000000..df8b6cae6 --- /dev/null +++ b/src/org/cyanogenmod/trebuchet/home/HomeWrapper.java @@ -0,0 +1,236 @@ +/* + * 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.trebuchet.home; + +import android.content.Context; +import android.util.Base64; +import android.util.SparseArray; +import android.view.View; + +import com.android.launcher.home.Home; + +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HomeWrapper { + + private static final int M_ID_ONSTART = 0; + private static final int M_ID_ONDESTROY = 1; + private static final int M_ID_ONRESUME = 2; + private static final int M_ID_ONPAUSE = 3; + private static final int M_ID_ONSHOW = 4; + private static final int M_ID_ONSCROLLPROGRESSCHANGED = 5; + private static final int M_ID_ONHIDE = 6; + private static final int M_ID_ONINVALIDATE = 7; + private static final int M_ID_ONREQUESTSEARCH = 8; + private static final int M_ID_CREATECUSTOMVIEW = 9; + private static final int M_ID_GETNAME = 10; + private static final int M_ID_GETNOTIFICATIONFLAGS = 11; + private static final int M_ID_GETOPERATIONFLAGS = 12; + private static final int M_LAST_ID = M_ID_GETOPERATIONFLAGS + 1; + + private final Context mContext; + private final Class<?> mClass; + private final Object mInstance; + + private final SparseArray<Method> cachedMethods; + + private final int mNotificationFlags; + private final int mOperationFlags; + + public HomeWrapper(Context context, Class<?> cls, Object instance) throws SecurityException { + super(); + mContext = context; + mClass = cls; + mInstance = instance; + cachedMethods = new SparseArray<Method>(M_LAST_ID); + + final String sha1 = createDigest(cls); + if (!sha1.equals(Home.SIGNATURE)) { + throw new SecurityException("The remote Home app doesn't implement " + + "the current Home Host Protocol. Signature: " + sha1); + } + + // Obtain the app flags + mNotificationFlags = getNotificationFlags(); + mOperationFlags = getOperationFlags(); + } + + /** @see Home#onStart(Context) **/ + public void onStart() { + invokeVoidContextMethod(M_ID_ONSTART, "onStart"); + } + + /** @see Home#onDestroy(Context) **/ + public void onDestroy() { + invokeVoidContextMethod(M_ID_ONDESTROY, "onDestroy"); + } + + /** @see Home#onResume(Context) **/ + public void onResume() { + if (!isNotificationSupported(Home.FLAG_NOTIFY_ON_RESUME)) { + return; + } + invokeVoidContextMethod(M_ID_ONRESUME, "onResume"); + } + + /** @see Home#onPause(Context) **/ + public void onPause() { + if (!isNotificationSupported(Home.FLAG_NOTIFY_ON_PAUSE)) { + return; + } + invokeVoidContextMethod(M_ID_ONPAUSE, "onPause"); + } + + /** @see Home#onShow(Context) **/ + public void onShow() { + if (!isNotificationSupported(Home.FLAG_NOTIFY_ON_SHOW)) { + return; + } + invokeVoidContextMethod(M_ID_ONSHOW, "onShow"); + } + + /** @see Home#onScrollProgressChanged(Context, float) **/ + public void onScrollProgressChanged(float progress) { + if (!isNotificationSupported(Home.FLAG_NOTIFY_ON_SCROLL_PROGRESS_CHANGED)) { + return; + } + try { + Method method = cachedMethods.get(M_ID_ONSCROLLPROGRESSCHANGED); + if (method == null) { + method = mClass.getMethod("onScrollProgressChanged", Context.class, float.class); + } + method.invoke(mInstance, mContext, progress); + } catch (ReflectiveOperationException ex) { + throw new SecurityException(ex); + } + } + + /** @see Home#onHide(Context) **/ + public void onHide() { + if (!isNotificationSupported(Home.FLAG_NOTIFY_ON_HIDE)) { + return; + } + invokeVoidContextMethod(M_ID_ONHIDE, "onHide"); + } + + /** @see Home#onInvalidate(Context) **/ + public void onInvalidate() { + invokeVoidContextMethod(M_ID_ONINVALIDATE, "onInvalidate"); + } + + /** + * @see Home#onRequestSearch(Context, int) + */ + public void onRequestSearch(int mode) { + try { + Method method = cachedMethods.get(M_ID_ONREQUESTSEARCH); + if (method == null) { + method = mClass.getMethod("onRequestSearch", Context.class, int.class); + } + method.invoke(mInstance, mContext, mode); + } catch (ReflectiveOperationException ex) { + throw new SecurityException(ex); + } + } + + /** @see Home#createCustomView(Context) **/ + public View createCustomView() { + try { + Method method = cachedMethods.get(M_ID_CREATECUSTOMVIEW); + if (method == null) { + method = mClass.getMethod("createCustomView", Context.class); + } + return (View) method.invoke(mInstance, mContext); + } catch (ReflectiveOperationException ex) { + throw new SecurityException(ex); + } + } + + /** @see Home#getName(Context) **/ + public String getName() { + try { + Method method = cachedMethods.get(M_ID_GETNAME); + if (method == null) { + method = mClass.getMethod("getName", Context.class); + } + return (String) method.invoke(mInstance, mContext); + } catch (ReflectiveOperationException ex) { + throw new SecurityException(ex); + } + } + + /** @see Home#getNotificationFlags() **/ + private int getNotificationFlags() { + try { + Method method = cachedMethods.get(M_ID_GETNOTIFICATIONFLAGS); + if (method == null) { + method = mClass.getMethod("getNotificationFlags"); + } + return (Integer) method.invoke(mInstance); + } catch (ReflectiveOperationException ex) { + return 0; + } + } + + /** @see Home#getOperationFlags() **/ + private int getOperationFlags() { + try { + Method method = cachedMethods.get(M_ID_GETOPERATIONFLAGS); + if (method == null) { + method = mClass.getMethod("getOperationFlags"); + } + return (Integer) method.invoke(mInstance); + } catch (ReflectiveOperationException ex) { + return 0; + } + } + + private void invokeVoidContextMethod(int methodId, String methodName) { + try { + Method method = cachedMethods.get(methodId); + if (method == null) { + method = mClass.getMethod(methodName, Context.class); + } + method.invoke(mInstance, mContext); + } catch (ReflectiveOperationException ex) { + throw new SecurityException(ex); + } + } + + private final String createDigest(Class<?> cls) throws SecurityException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + Method[] methods = cls.getDeclaredMethods(); + for (Method method : methods) { + digest.update(method.toString().getBytes()); + } + return new String(Base64.encode(digest.digest(), Base64.NO_WRAP)); + } catch (NoSuchAlgorithmException ex) { + throw new SecurityException(ex); + } + } + + public boolean isNotificationSupported(int flag) { + return (mNotificationFlags & flag) == flag; + } + + public boolean isOperationSupported(int flag) { + return (mOperationFlags & flag) == flag; + } +} |