diff options
author | Jorge Ruesga <jorge@ruesga.com> | 2014-02-24 01:04:33 +0100 |
---|---|---|
committer | Matt Garnes <matt@cyngn.com> | 2014-06-17 17:38:02 -0700 |
commit | 56db21e5af4c17f9308ceb07b0f3ab651743b5cd (patch) | |
tree | 03d9c1b1a761c78a277d888b3698ece86826df07 /src/org | |
parent | cf43a17640a7eede4106670f97e0834b5ea0f976 (diff) | |
download | android_packages_apps_Trebuchet-56db21e5af4c17f9308ceb07b0f3ab651743b5cd.tar.gz android_packages_apps_Trebuchet-56db21e5af4c17f9308ceb07b0f3ab651743b5cd.tar.bz2 android_packages_apps_Trebuchet-56db21e5af4c17f9308ceb07b0f3ab651743b5cd.zip |
trebuchet: custom home
Change-Id: I4faee66580ab0e41ee8e8bcbd79ce680d45bce97
Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
Diffstat (limited to 'src/org')
-rw-r--r-- | src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java | 333 | ||||
-rw-r--r-- | src/org/cyanogenmod/trebuchet/home/HomeUtils.java | 112 | ||||
-rw-r--r-- | src/org/cyanogenmod/trebuchet/home/HomeWrapper.java | 236 |
3 files changed, 681 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java b/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java new file mode 100644 index 000000000..12e04cb16 --- /dev/null +++ b/src/org/cyanogenmod/trebuchet/TrebuchetLauncher.java @@ -0,0 +1,333 @@ +/* + * 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; + +public class TrebuchetLauncher extends Launcher { + + private static final String TAG = "TrebuchetLauncher"; + + private static final boolean DEBUG = true; + + 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() { + updateQsbBarColorState(0); + 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() { + updateQsbBarColorState(255); + 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(0); + + // 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; + } + + @Override + protected void invalidateHasCustomContentToLeft() { + invalidateHomeStub(); + super.invalidateHasCustomContentToLeft(); + } + + @Override + protected void addCustomContentToLeft() { + 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); + } else { + voiceButton.clearColorFilter(); + } + } + ImageView searchButton = getQsbBarSearchButton(); + if (searchButton != null) { + if (progress > 0) { + searchButton.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } else { + searchButton.clearColorFilter(); + } + } + } + } +} 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; + } +} |