/* * Copyright 2015. Appsi Mobile * * 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.appsimobile.appsii; import android.animation.Animator; import android.annotation.TargetApi; import android.app.KeyguardManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.database.ContentObserver; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.util.CircularArray; import android.support.v4.widget.ScrollerCompat; import android.util.Log; import android.view.VelocityTracker; import android.view.View; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.DecelerateInterpolator; import android.widget.RelativeLayout; import com.appsimobile.appsii.Sidebar.SidebarListener; import com.appsimobile.appsii.SidebarHotspot.SwipeListener; import com.appsimobile.appsii.dagger.AppInjector; import com.appsimobile.appsii.dagger.AppsiComponent; import com.appsimobile.appsii.dagger.AppsiInjector; import com.appsimobile.appsii.dagger.AppsiModule; import com.appsimobile.appsii.dagger.DaggerAppsiComponent; import com.appsimobile.appsii.hotspotmanager.ManageHotspotsActivity; import com.appsimobile.appsii.icontheme.iconpack.ActiveIconPackInfo; import com.appsimobile.appsii.module.home.config.HomeItemConfigurationHelper; import com.appsimobile.appsii.module.home.provider.HomeContract; import com.appsimobile.appsii.permissions.PermissionUtils; import com.appsimobile.appsii.plugins.IconCache; import com.appsimobile.appsii.preference.PreferenceHelper; import java.lang.ref.WeakReference; import javax.inject.Inject; /** * Appsii's main service. * <p/> * To see the status: adb shell dumpsys activity services com.appsimobile.appsii */ public class Appsi extends Service implements OnSharedPreferenceChangeListener, HotspotHelperListener, SidebarListener, PopupLayer.PopupLayerListener, Sidebar.OnCancelCloseListener { /** * Notification Ids */ public static final int ONGOING_NOTIFICATION_ID = 11; /** * Will be sent to Appsi to exit the main service */ public static final String ACTION_STOP_APPSI = BuildConfig.APPLICATION_ID + ".ACTION_STOP_APPSI"; /** * Will be sent to Appsi to restart the main service. For example after a theme change */ public static final String ACTION_RESTART_APPSI = BuildConfig.APPLICATION_ID + ".ACTION_RESTART_APPSI"; /** * Broadcast to send to Appsi to resume all hotspots */ public static final String ACTION_UNSUSPEND = BuildConfig.APPLICATION_ID + ".UNSUSPEND_APPSI"; /** * Sent to Appsi to close the sidebar */ public static final String ACTION_CLOSE_SIDEBAR = BuildConfig.APPLICATION_ID + ".action_close_sidebar"; /** * Sent to Appsi to close the sidebar */ public static final String ACTION_TRY_PAGE = BuildConfig.APPLICATION_ID + ".action_try"; /** * Broadcast to get the status from Appsi */ public static final String ACTION_ORDERED_BROADCAST_RUNNING = "com.appsimobile.appsii.ACTION_RUNNING"; public static final int RESULT_RUNNING_STATUS_SUSPENDED = 3; // ORDERED BROADCASTS public static final int RESULT_RUNNING_STATUS_ENABLED = 4; public static final int RESULT_RUNNING_STATUS_DISABLED = 5; public static final String ACTION_LOCAL_OPEN_SIDEBAR_FROM_SHORTCUT = "com.appsimobile.appsii.ACTION_LOCAL_OPEN_SIDEBAR_FROM_SHORTCUT"; /** * FLAG for appsi to indicate it should open from the left */ public static final int OPEN_FLAG_LEFT = 2; /** * FLAG for appsi to indicate it should open from the right */ public static final int OPEN_FLAG_RIGHT = 4; /** * FLAG for appsi to indicate it should open from the right */ public static final int OPEN_FLAG_LIKE_NOTIFICATION_BAR = 8; static final int MESSAGE_CLOSE_SIDEBAR = 101; private static final String TAG = "Appsi"; // RECEIVERS /** * True if Appsi was suspended by the user */ static volatile boolean mSuspended = false; /** * Used in the binder to determine if Appsi was already created. * This will be used to determine it's status */ static volatile boolean mCreated = false; private final AccelerateInterpolator mOutInterpolator = new AccelerateInterpolator(); // CONFIGURATION OPTIONS private final DecelerateInterpolator mInInterpolator = new DecelerateInterpolator(); /** * Receives and handles the following LOCAL broadcasts: * {@link #ACTION_LOCAL_OPEN_SIDEBAR_FROM_SHORTCUT} */ LocalReceiver mLocalActionsReceiver; /** * The percentage of screen space the sidebar should be wide */ int mSidebarPercentage; /** * A reference to the action sidebar view */ @Inject Sidebar mSidebar; // VIEWS /** * The layer we add views to. This layer automatically handles things like * showing and hiding the screen dimming */ @Inject PopupLayer mPopupLayer; private final Animator.AnimatorListener mCloseListener = new AnimatorAdapter() { @Override public void onAnimationEnd(Animator animation) { mPopupLayer.removePopupChild(mSidebar); mSidebar.setTranslationX(0); } }; // RUNTIME STATE /** * A handler used to post certain events to. And for content observers */ Handler mHandler; /** * Handles changes in the hotspots. Will remove, add and update hotspots as * needed. */ private final ContentObserver mHotspotsContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { onChange(selfChange, null); } // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument. @Override public void onChange(boolean selfChange, Uri uri) { // simple reload the hotspots reloadHotspots(); } }; /** * Used to query the keyguard status. When the key-guard is showing Appsi will * not be shown. */ @Inject KeyguardManager mKeyguardManager; /** * The window manager. This is a system service used to show views on top of * other apps. */ @Inject WindowManager mWindowManager; @Inject IconCache mIconCache; @Inject PermissionUtils mPermissionUtils; /** * The total number of plugins currently added. */ int mConfiguredPluginCount; /** * The last known configuration appsi was shown in */ Configuration mLastConfiguration; /** * A view that is dragged across the screen that looks like the notification * opening handle. */ NotificationLikeOpener mNotificationLikeOpener; AnimationListener mScrollAnimationListener; /** * The shared preferences for Appsi */ SharedPreferences mPrefs; /** * Handles things like showing/hiding the hotspots and flashing the hotspots. * In it's early days, Appsi had two operating modes, one for a hotspot per * plugin and one for a single hotspot. This was removed later when Appsi * evolved. */ @Inject AbstractHotspotHelper mHotspotHelper; @Inject LoaderManagerImpl mLoaderManager; @Inject PreferenceHelper mPreferenceHelper; @Inject HomeItemConfigurationHelper mHomeItemConfigurationHelper; AppsiComponent mApplicationComponent; /** * receives and handles several broadcasts and updates Appsi accordingly. This * mostly involves things like removing an open sidebar when the screen is turned * of, closing system dialogs when requested, day-dreams etc. * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE} * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} * {@link android.content.Intent#ACTION_USER_PRESENT} * {@link android.content.Intent#ACTION_DREAMING_STARTED} * {@link android.content.Intent#ACTION_SCREEN_ON} * {@link android.content.Intent#ACTION_CLOSE_SYSTEM_DIALOGS} * {@link android.content.Intent#ACTION_SCREEN_OFF} */ private ExternalEventsListener mExternalEventsReceiver; /** * Handles the following LOCAL broadcasts: * {@link #ACTION_ORDERED_BROADCAST_RUNNING} * {@link #ACTION_STOP_APPSI} */ private AppsiStatusBroadcastReceiver mIsRunningBroadcastReceiver; /** * Handles plugin installation events */ private BroadcastReceiver mAppStatusReceiver; /** * True while the sidebar is visible. */ private boolean mSidebarVisible; /** * The hotspot configurations that have been found in the * appsi database. */ private CircularArray<HotspotItem> mHotspotItems; public Appsi() { } @Override public void onCreate() { Log.d(TAG, "onCreate() called with: " + ""); super.onCreate(); Log.d(TAG, "onCreate() create shared prefs"); mPrefs = AppInjector.provideSharedPreferences(); Context context = ThemingUtils.createContextThemeWrapper(this, mPrefs); Log.d(TAG, "onCreate() initializeDagger"); initializeDagger(context); mHotspotHelper.setCallback(this); Log.d(TAG, "onCreate() configure popup layer"); mPopupLayer.setPopuplayerListener(this); mNotificationLikeOpener = new NotificationLikeOpener(this); Log.d(TAG, "onCreate() create configuration"); mLastConfiguration = new Configuration(getResources().getConfiguration()); Log.d(TAG, "onCreate() configure sidebar"); mSidebar.setOnCancelCloseListener(this); mSidebar.setSidebarListener(this); Log.d(TAG, "onCreate() preferences"); mSidebarPercentage = mPreferenceHelper.getSidebarWidth(); int value = mPreferenceHelper.getSidebarDimLevel(); updateDefaultDimColor(ThemingUtils.getPercentage(value)); mPrefs.registerOnSharedPreferenceChangeListener(this); Log.d(TAG, "onCreate() create External events receiver"); mExternalEventsReceiver = new ExternalEventsListener(); mExternalEventsReceiver.register(this); Log.d(TAG, "onCreate() create Runnig receiver"); mIsRunningBroadcastReceiver = new AppsiStatusBroadcastReceiver(); mIsRunningBroadcastReceiver.register(this); Log.d(TAG, "onCreate() create status receiver"); mAppStatusReceiver = AppStatusReceiver.register(this); Log.d(TAG, "onCreate() create local receiver"); mLocalActionsReceiver = new LocalReceiver(); mLocalActionsReceiver.register(this); Log.d(TAG, "onCreate() Handler"); mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == MESSAGE_CLOSE_SIDEBAR) { onCloseSidebar(); } return false; } }); // listen for changes in the hotspots Log.d(TAG, "onCreate() registering contentObserver"); getContentResolver().registerContentObserver(HomeContract.Hotspots.CONTENT_URI, true, mHotspotsContentObserver); // Now that appsi has been initialized, scan the hotspot config Log.d(TAG, "onCreate() Load hotspots"); reloadHotspots(); mCreated = true; Log.d(TAG, "onCreate() completed onCreate…"); } private void updateDefaultDimColor(float alpha) { mPopupLayer.setDefaultDimAlpha(alpha); } @Override public void onCloseSidebar() { // remove any possible messages that are still scheduled to close Appsii mHandler.removeMessages(MESSAGE_CLOSE_SIDEBAR); if (mSidebarVisible) { mSidebarVisible = false; boolean left = mSidebar.getIsLeft(); int sidebarWidth = getSidebarWidth(); mSidebar.animate(). translationX(left ? -sidebarWidth : sidebarWidth). setListener(mCloseListener). setInterpolator(mOutInterpolator). setDuration(150); mNotificationLikeOpener.mRemoveAfterScrollEnd = false; mSidebar.onSidebarClosing(); } if (mLoaderManager.isStarted()) { mLoaderManager.doStop(); } } /** * Start an AsyncTask that will load the hotspot configurations. * This will call {@link #onHotspotsLoaded(CircularArray)} when finished */ void reloadHotspots() { HotspotLoader loader = new HotspotLoader(this); loader.execute(); } private int getSidebarWidth() { int w = mWindowManager.getDefaultDisplay().getWidth(); int h = mWindowManager.getDefaultDisplay().getHeight(); int sw = Math.min(w, h); return (sw * mSidebarPercentage) / 100; } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand() called with: " + "intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); startAppsiService(); // force initialize the home config to make everything a bit smoother // and this makes sure we don't have to wait for it when opening the // sidebar for the first time. // mHomeItemConfigurationHelper String action = intent == null ? null : intent.getAction(); if (ACTION_TRY_PAGE.equals(action)) { HotspotPageEntry entry = intent.getParcelableExtra("entry"); HotspotItem hotspotItem = intent.getParcelableExtra("hotspot"); if (entry != null && hotspotItem != null) { CircularArray<HotspotPageEntry> entries = new CircularArray<>(1); entries.addFirst(entry); openSidebar(hotspotItem, entries, 0, true); } } return Service.START_STICKY; } @Override public void onDestroy() { super.onDestroy(); mPrefs.unregisterOnSharedPreferenceChangeListener(this); LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalActionsReceiver); unregisterReceiver(mExternalEventsReceiver); unregisterReceiver(mAppStatusReceiver); unregisterReceiver(mIsRunningBroadcastReceiver); getContentResolver().unregisterContentObserver(mHotspotsContentObserver); mPopupLayer.setPopuplayerListener(null); mSidebar.setSidebarListener(null); mSidebar = null; mPopupLayer = null; mSuspended = false; mHotspotHelper.removeHotspots(); mHotspotHelper.onDestroy(); Intent i = new Intent(AppsiiUtils.ACTION_APPSI_STATUS_CHANGED); sendBroadcast(i); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); int diff = mLastConfiguration.diff(newConfig); // handle layout-direction change boolean directionChanged = (diff & ActivityInfo.CONFIG_LAYOUT_DIRECTION) == ActivityInfo.CONFIG_LAYOUT_DIRECTION; boolean localeChanged = (diff & ActivityInfo.CONFIG_LOCALE) == ActivityInfo.CONFIG_LOCALE; boolean orientationChanged = newConfig.orientation != mLastConfiguration.orientation; mLastConfiguration = new Configuration(newConfig); if (directionChanged || localeChanged) { Log.i("Appsi", "restarting because of direction or locale change"); restartAppsiService(); return; } if (orientationChanged) { mHotspotHelper.onOrientationChanged(); } } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); if (mSidebar != null) { mSidebar.onTrimMemory(level); } } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void startAppsiService() { Log.d(TAG, "Starting Appsi…"); updateNotificationStatus(); mHandler.postDelayed(new Runnable() { @Override public void run() { Intent i = new Intent(AppsiiUtils.ACTION_STARTED); sendBroadcast(i); } }, 500); } public SidebarHotspot.SwipeListener openSidebar(HotspotItem conf, CircularArray<HotspotPageEntry> entries, int flags, boolean animate) { LockableAsyncQueryHandler.lock(); mSidebar.setSidebarOpening(true); if (!mLoaderManager.isStarted()) { mLoaderManager.doStart(); } boolean fullscreen = mHotspotHelper.getTopOffset() == 0; mSidebar.setInFullScreenMode(fullscreen); mNotificationLikeOpener.mRemoveAfterScrollEnd = false; mScrollAnimationListener = null; mSidebar.animate().cancel(); // users reported this may happen if (mSidebarVisible) { mPopupLayer.forceClose(); mSidebarVisible = false; } int sidebarWidth = getSidebarWidth(); mSidebarVisible = true; if (conf == null) { conf = getDefaultConfiguration(); } SwipeListener result = null; boolean openLikeNotificationBar = (flags & OPEN_FLAG_LIKE_NOTIFICATION_BAR) != 0; mSidebar.setTag(conf); boolean left = conf.mLeft; boolean forceLeft = (flags & OPEN_FLAG_LEFT) != 0; boolean forceRight = (flags & OPEN_FLAG_RIGHT) != 0; if (forceLeft) { left = true; } if (forceRight) { left = false; } mSidebar.setIsLeft(left); View v = mSidebar.findViewById(R.id.sidebar_content); RelativeLayout.LayoutParams sidebarLayoutParams = (RelativeLayout.LayoutParams) v.getLayoutParams(); sidebarLayoutParams.width = sidebarWidth; View container = mSidebar.findViewById(R.id.sidebar_container); RelativeLayout.LayoutParams slp = (RelativeLayout.LayoutParams) container.getLayoutParams(); if (left) { slp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0); slp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); sidebarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0); sidebarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); } else { slp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0); slp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); sidebarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0); sidebarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); } if (animate && !openLikeNotificationBar) { mNotificationLikeOpener.cancel(); if (left) { mSidebar.setTranslationX(-sidebarWidth); mSidebar.animate(). setInterpolator(mInInterpolator).setDuration(400). setListener(null).translationX(0); mSidebar.findViewById(R.id.sidebar_left_shadow).setVisibility(View.GONE); mSidebar.findViewById(R.id.sidebar_right_shadow).setVisibility(View.VISIBLE); } else { mSidebar.setTranslationX(sidebarWidth); mSidebar.animate().setDuration(400). setListener(null). setInterpolator(mInInterpolator).translationX(0); mSidebar.findViewById(R.id.sidebar_left_shadow).setVisibility(View.VISIBLE); mSidebar.findViewById(R.id.sidebar_right_shadow).setVisibility(View.GONE); } updateDimColor(mSidebar, left); mPopupLayer.addPopupChild(mSidebar); mSidebar.updateAdapterData(conf.mDefaultPageId, entries); mScrollAnimationListener = null; } else if (openLikeNotificationBar) { result = mNotificationLikeOpener; if (left) { mSidebar.findViewById(R.id.sidebar_right_shadow).setVisibility(View.VISIBLE); mSidebar.findViewById(R.id.sidebar_left_shadow).setVisibility(View.GONE); } else { mSidebar.findViewById(R.id.sidebar_right_shadow).setVisibility(View.GONE); mSidebar.findViewById(R.id.sidebar_left_shadow).setVisibility(View.VISIBLE); } int w = mWindowManager.getDefaultDisplay().getWidth(); int h = mWindowManager.getDefaultDisplay().getHeight(); int sw = Math.min(w, h); mNotificationLikeOpener.setTargetView(mSidebar, sidebarWidth, sw, left); mSidebar.updateAdapterData(conf.mDefaultPageId, entries); mPopupLayer.addPopupChild(mSidebar); } else { mSidebar.setTranslationX(0); mPopupLayer.addPopupChild(mSidebar); } return result; } protected void updateNotificationStatus() { Log.d(TAG, "starting foreground"); if (mSuspended) { startForeground(ONGOING_NOTIFICATION_ID, createSuspendedNotification()); } else { stopForeground(false); startForeground(ONGOING_NOTIFICATION_ID, createOperatingNormallyNotification()); } } private HotspotItem getDefaultConfiguration() { return mHotspotItems.isEmpty() ? null : mHotspotItems.get(0); } float updateDimColor(View scrollView, boolean left) { if (scrollView != null) { int mTargetWidth = scrollView.getWidth(); float factor; int scroll = (int) -scrollView.getTranslationX(); float openedPercentage; if (left) { scroll = (int) (scroll + (0 * getResources().getDisplayMetrics().density)); factor = scroll / (float) mTargetWidth; openedPercentage = 1 - factor; } else { scroll = (int) (scroll - (0 * getResources().getDisplayMetrics().density)); factor = (mTargetWidth - scroll) / (float) mTargetWidth; openedPercentage = 1 - (factor - 1); } mPopupLayer.setDimLayerAlpha(1); return openedPercentage; } return 0; } Notification createSuspendedNotification() { Intent resultIntent = new Intent(ACTION_UNSUSPEND); PendingIntent bc = PendingIntent.getBroadcast(this, 0, resultIntent, 0); return createNotification(bc, R.string.notification_suspended, R.drawable.appsi_notification_icon_alert).build(); } Notification createOperatingNormallyNotification() { Intent resultIntent = new Intent(this, MainActivity.class); PendingIntent resultPendingIntent = createPendingIntentWithBackstack(resultIntent); boolean minimalNotification = mPrefs.getBoolean("pref_minimal_notification", false); int desc = R.string.notification_description; int drawable = R.drawable.ic_notification; NotificationCompat.Builder b = createNotification(resultPendingIntent, desc, drawable); // When the user requested a minimal notification, do not add additional actions // to it. Simply show the running notification. if (!minimalNotification) { // When the normal notification must be shown, build a large text notification // and add the actions to it. NotificationCompat.BigTextStyle largeNotification = new NotificationCompat.BigTextStyle(); largeNotification.setBigContentTitle(getString(R.string.application_name)); largeNotification.setSummaryText(getString(R.string.notification_description)); b.setStyle(largeNotification); // hotspots configuration action Intent hotspotIntent = new Intent(this, ManageHotspotsActivity.class); PendingIntent hsPendingIntent = createPendingIntentWithBackstack(hotspotIntent); b.addAction(R.drawable.ic_action_panels, getString(R.string.hotspots_and_pages), hsPendingIntent); b.setPriority(NotificationCompat.PRIORITY_MIN); } else { b.setWhen(0); b.setPriority(NotificationCompat.PRIORITY_MIN); } // don't show on wearables, there is no use for that b.setLocalOnly(true); return b.build(); } /** * Creates a simple notification without any additional actions */ NotificationCompat.Builder createNotification(PendingIntent action, int descriptionResourceId, int imageResourceId) { NotificationCompat.Builder b = new NotificationCompat.Builder(this); b.setSmallIcon(imageResourceId); b.setContentTitle(getString(R.string.application_name)); b.setContentText(getString(descriptionResourceId)); b.setContentIntent(action); // don't show on wearables, there is no use for that b.setLocalOnly(true); return b; } /** * Creates a pending intent that can be added to an action in the notification */ PendingIntent createPendingIntentWithBackstack(Intent resultIntent) { // The stack builder object will contain an artificial back stack for the // started Activity. // This ensures that navigating backward from the Activity leads out of // your application to the Home screen. TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); // Adds the back stack for the Intent (but not the Intent itself) stackBuilder.addParentStack(MainActivity.class); // Adds the Intent that starts the Activity to the top of the stack stackBuilder.addNextIntent(resultIntent); return stackBuilder.getPendingIntent( 0, PendingIntent.FLAG_UPDATE_CURRENT ); } String levelToString(int level) { switch (level) { case TRIM_MEMORY_BACKGROUND: return "TRIM_MEMORY_BACKGROUND"; case TRIM_MEMORY_COMPLETE: return "TRIM_MEMORY_COMPLETE"; case TRIM_MEMORY_MODERATE: return "TRIM_MEMORY_MODERATE"; case TRIM_MEMORY_UI_HIDDEN: return "TRIM_MEMORY_UI_HIDDEN"; case TRIM_MEMORY_RUNNING_CRITICAL: return "TRIM_MEMORY_RUNNING_CRITICAL"; case TRIM_MEMORY_RUNNING_LOW: return "TRIM_MEMORY_RUNNING_LOW"; case TRIM_MEMORY_RUNNING_MODERATE: return "TRIM_MEMORY_RUNNING_LOW"; } return String.valueOf(level); } void restartAppsiService() { stopAppsiService(); Intent intent = new Intent(this, Appsi.class); startService(intent); } void stopAppsiService() { Log.d(TAG, "stopAppsiService() called with: " + ""); onSuspend(); mCreated = true; mHotspotHelper.removeHotspots(); stopForeground(true); mLoaderManager.doDestroy(); stopSelf(); removeHotspots(); AppsiInjector.setAppsiComponent(null); } void onSuspend() { removeHotspots(); mPopupLayer.onSuspend(); Intent i = new Intent(AppsiiUtils.ACTION_SUSPEND); sendBroadcast(i); } void removeHotspots() { mHotspotHelper.removeHotspots(); } /** * Updates Appsi's status to or from suspended. */ public void setSuspended(boolean suspended) throws PermissionDeniedException { if (mSuspended != suspended) { mSuspended = suspended; updateNotificationStatus(); if (suspended) { onSuspend(); } else { onUnsuspend(); } } } void onUnsuspend() throws PermissionDeniedException { addHotspotsIfUnlocked(); Intent i = new Intent(AppsiiUtils.ACTION_UNSUSPEND); sendBroadcast(i); } void addHotspotsIfUnlocked() throws PermissionDeniedException { if (!isKeyguardLocked() && !mSuspended) { addHotspots(); } } boolean isKeyguardLocked() { return mKeyguardManager.isKeyguardLocked(); } void addHotspots() throws PermissionDeniedException { if (!mSuspended) { mHotspotHelper.addHotspots(); } } /** * Called when loading the hotspots completes. This will remove currently * visible hotspots, and add new ones in case the screen is not locked */ public void onHotspotsLoaded(CircularArray<HotspotItem> configurations) { mHotspotItems = configurations; mConfiguredPluginCount = configurations.size(); updateNotificationStatus(); mHotspotHelper.onHotspotsLoaded(configurations); removeHotspots(); try { addHotspotsIfUnlocked(); } catch (PermissionDeniedException e) { // STOPSHIP: FIXME: this must be changed! onPermissionDenied(e); } } void openSidebarFromShortcut(Intent intent) { // final Uri uri = Uri.parse(intent.getStringExtra(ShortcutActivity.EXTRA_DATASET_URI)); // final boolean left = // intent.getBooleanExtra(ShortcutActivity.EXTRA_ACTION_TO_PERFORM, false); // mHandler.postDelayed(new Runnable() { // @Override // public void run() { // openSidebar(null, uri, left ? OPEN_FLAG_LEFT : OPEN_FLAG_RIGHT, true); // } // }, 1); // } void onOpenCompleted() { LockableAsyncQueryHandler.unlock(); mSidebar.setSidebarOpening(false); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { switch (key) { case "pref_minimal_notification": onNotificationOptionsChanged(); break; case "pref_sidebar_dimming_level": int value = mPreferenceHelper.getSidebarDimLevel(); updateDefaultDimColor(ThemingUtils.getPercentage(value)); break; case "pref_icon_theme": // TODO: add this again String iconPackStringUri = sharedPreferences.getString("pref_icon_theme", null); Uri iconPackUri = iconPackStringUri == null ? null : Uri.parse(iconPackStringUri); ActiveIconPackInfo.getInstance(this).setActiveIconPackUri(iconPackUri); mIconCache.clearAllIcons(); break; case "pref_hotspot_width": removeHotspots(); try { addHotspotsIfUnlocked(); } catch (PermissionDeniedException e) { Log.wtf(TAG, "permission denied?", e); } break; case "pref_hide_persistent_notification_with_hack": onNotificationOptionsChanged(); break; case "pref_sidebar_size": mSidebarPercentage = mPreferenceHelper.getSidebarWidth(); break; case "pref_sidebar_haptic_feedback": boolean mVibrate = mPreferenceHelper.getHotspotsHapticFeedbackEnabled(); mHotspotHelper.setVibrate(mVibrate); break; } } private void onNotificationOptionsChanged() { updateNotificationStatus(); } @Override public void onPopupLayerForceClosed() { mSidebarVisible = false; } @Override public void opPopupLayerHidden() throws PermissionDeniedException { addHotspotsIfUnlocked(); } @Override public void opPopupLayerShown() { removeHotspots(); } @Override public SwipeListener openSidebar(HotspotItem conf, CircularArray<HotspotPageEntry> entries, int flags) { return openSidebar(conf, entries, flags, true /* animate */); } @Override public boolean canShowSidebar(HotspotItem configuration) { return true; } @Override public void cancelVisualHints() { } @Override public void startActivity(Intent intent) { onCloseSidebar(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); super.startActivity(intent); } @Override public void startActivity(Intent intent, Bundle options) { onCloseSidebar(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); super.startActivity(intent, options); } @Override public void startIntentSender(IntentSender intent, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { closeAfterAppWidgetAction(); super.startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags); } private void closeAfterAppWidgetAction() { mSidebar.showCloseOverlay(new CloseCallbackImpl()); } @Override public void startIntentSender(IntentSender intent, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException { closeAfterAppWidgetAction(); super.startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags, options); } /** * There seems to be a problem unregistering receivers sometimes. * This is definitely a bug in Appsi. For now work around it by * catching the exception to make sure Appsi won't crash. */ @Override public void unregisterReceiver(BroadcastReceiver receiver) { try { super.unregisterReceiver(receiver); } catch (Exception e) { Log.wtf("Appsi", "error unregistering receiver", e); } } @Override public void onCloseCancelled() { mHandler.removeMessages(MESSAGE_CLOSE_SIDEBAR); } void onPermissionDenied(PermissionDeniedException e) { Log.d(TAG, "No permission", e); mPermissionUtils.showPermissionNotification(this, 1001, android.Manifest.permission.SYSTEM_ALERT_WINDOW, 0); stopAppsiService(); } protected void initializeDagger(Context themedContext) { mApplicationComponent = DaggerAppsiComponent .builder() .appsiModule(new AppsiModule(this, themedContext)) .build(); AppsiInjector.setAppsiComponent(mApplicationComponent); // mSidebar = AppsiInjector.provideSidebar(); // mPopupLayer = AppsiInjector.providePopupLayer(); // mHotspotHelper = AppsiInjector.provideHotspotHelper(); AppsiInjector.inject(this); } static class HotspotLoader extends AsyncTask<Void, Void, CircularArray<HotspotItem>> { private final WeakReference<Appsi> mContext; public HotspotLoader(Appsi c) { mContext = new WeakReference<>(c); } @Override protected CircularArray<HotspotItem> doInBackground(Void... params) { Context context = mContext.get(); if (context == null) return null; return com.appsimobile.appsii.module.HotspotLoader.loadHotspots(context); } @Override protected void onPostExecute(CircularArray<HotspotItem> result) { Appsi appsi = mContext.get(); if (appsi != null) { appsi.onHotspotsLoaded(result); } } } class NotificationLikeOpener implements SidebarHotspot.SwipeListener { final int mVelocityTreshold; final ScrollerCompat mScrollerCompat; int mTargetWidth; int mScreenWidth; boolean mLeft; long mLastLocationUpdate; boolean mRemoveAfterScrollEnd; // Handler mHandler; WeakReference<View> mTargetView; private final Runnable mUpdatePositionRunnable = new Runnable() { @Override public void run() { boolean running = mScrollerCompat.computeScrollOffset(); int x = mScrollerCompat.getCurrX(); View targetView = mTargetView.get(); mScrollerCompat.getFinalX(); if (targetView != null) { targetView.setTranslationX(-x); updateDimColor(); if (running) { targetView.postOnAnimation(this); } else { targetView.setTranslationX(-mScrollerCompat.getFinalX()); if (mRemoveAfterScrollEnd) { mPopupLayer.removePopupChild(mSidebar); } else { onScrollEnded(targetView); } } } } }; public NotificationLikeOpener(Context context) { // mHandler = new Handler(); mVelocityTreshold = (int) (context.getResources().getDisplayMetrics().density * 125); mScrollerCompat = ScrollerCompat.create(context, new DecelerateInterpolator()); } @Override public void setSwipeLocation(SidebarHotspot hotspot, int localX, int localY, int screenX, int screenY) { mRemoveAfterScrollEnd = false; // position the targetview according to the drag View targetView = mTargetView.get(); int delta = mTargetWidth - mScreenWidth; if (targetView != null) { int xValue = localX > mTargetWidth ? mTargetWidth : localX; if (mLeft) { targetView.setTranslationX(-(mScreenWidth - xValue + delta)); } else { int x = -xValue - mTargetWidth; if (x > 0) { x = 0; } targetView.setTranslationX(-x); } } updateDimColor(); } float updateDimColor() { View view = mTargetView.get(); if (view != null) { return Appsi.this.updateDimColor(view, mLeft); } return 0; } @Override public void onSwipeEnd(SidebarHotspot hotspot, int screenX, int screenY, boolean cancelled, VelocityTracker velocityTracker) { // snap to target or close // fade out drag handle View targetView = mTargetView.get(); velocityTracker.computeCurrentVelocity(1000); float xVelocity = velocityTracker.getXVelocity(); float pct = updateDimColor(); boolean isOpened = false; if (targetView != null) { if (mLeft) { int threshold = pct >= .2f ? -mVelocityTreshold : mVelocityTreshold; if (xVelocity > threshold) { mScrollerCompat.startScroll((int) -targetView.getTranslationX(), 0, (int) targetView.getTranslationX(), 0); isOpened = true; // open } else { mScrollerCompat.startScroll((int) -targetView.getTranslationX(), 0, (int) (mTargetWidth + targetView.getTranslationX()), 0, 100); // close } } else { int threshold = pct >= .2f ? mVelocityTreshold : -mVelocityTreshold; if (xVelocity < threshold) { mScrollerCompat.startScroll((int) -targetView.getTranslationX(), 0, (int) targetView.getTranslationX(), 0); isOpened = true; // open } else { mScrollerCompat.startScroll((int) -targetView.getTranslationX(), 0, (int) (-mTargetWidth + targetView.getTranslationX()), 0, 100); // close } } mRemoveAfterScrollEnd = !isOpened; targetView.postOnAnimation(mUpdatePositionRunnable); } } void onScrollEnded(View targetView) { if (targetView.getTranslationX() == 0) { onOpenCompleted(); } } public void setTargetView(View v, int targetWidth, int minScreenWidth, boolean left) { mTargetView = new WeakReference<>(v); mTargetWidth = targetWidth; mScreenWidth = minScreenWidth; mLeft = left; mLastLocationUpdate = System.currentTimeMillis(); if (left) { v.setTranslationX(-targetWidth); } else { v.setTranslationX(targetWidth); } updateDimColor(); mLastLocationUpdate = System.currentTimeMillis(); } public void cancel() { mScrollerCompat.abortAnimation(); } } class LocalReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_LOCAL_OPEN_SIDEBAR_FROM_SHORTCUT.equals(action)) { openSidebarFromShortcut(intent); } } void register(Context context) { IntentFilter reloadFilter = new IntentFilter(ACTION_LOCAL_OPEN_SIDEBAR_FROM_SHORTCUT); LocalBroadcastManager mgr = LocalBroadcastManager.getInstance(context); mgr.registerReceiver(mLocalActionsReceiver, reloadFilter); } } class AppsiStatusBroadcastReceiver extends BroadcastReceiver { public void register(Context appsi) { IntentFilter isRunningIntentFilter = new IntentFilter(); isRunningIntentFilter.addAction(ACTION_STOP_APPSI); isRunningIntentFilter.addAction(ACTION_RESTART_APPSI); isRunningIntentFilter.addAction(ACTION_ORDERED_BROADCAST_RUNNING); appsi.registerReceiver(this, isRunningIntentFilter); } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_ORDERED_BROADCAST_RUNNING.equals(action)) { if (mSuspended) { setResultCode(RESULT_RUNNING_STATUS_SUSPENDED); } else { setResultCode(RESULT_RUNNING_STATUS_ENABLED); } } else if (ACTION_RESTART_APPSI.equals(action)) { restartAppsiService(); } else if (ACTION_STOP_APPSI.equals(action)) { stopAppsiService(); } } } class ExternalEventsListener extends BroadcastReceiver { void register(Context context) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_PRESENT); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); intentFilter.addAction(Intent.ACTION_SCREEN_ON); intentFilter.addAction(ACTION_CLOSE_SIDEBAR); intentFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); addApi8Features(intentFilter); addApi17Features(intentFilter); context.registerReceiver(this, intentFilter); } @TargetApi(Build.VERSION_CODES.FROYO) private void addApi8Features(IntentFilter appActionFilter) { appActionFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); appActionFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); } private void addApi17Features(IntentFilter intentFilter) { intentFilter.addAction(Intent.ACTION_DREAMING_STARTED); intentFilter.addAction(Intent.ACTION_DREAMING_STOPPED); } @Override public void onReceive(Context context, Intent intent) { try { onReceiveImpl(context, intent); } catch (PermissionDeniedException e) { onPermissionDenied(e); } } public void onReceiveImpl(Context context, Intent intent) throws PermissionDeniedException { String action = intent.getAction(); switch (action) { case ACTION_CLOSE_SIDEBAR: onCloseSidebar(); addHotspotsIfUnlocked(); break; case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE: AppStatusReceiver.broadcastRefreshAllApps(context); break; case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE: AppStatusReceiver.broadcastRefreshAllApps(context); break; case Intent.ACTION_USER_PRESENT: // show our hotspot addHotspots(); break; case Intent.ACTION_DREAMING_STARTED: removeHotspots(); break; case Intent.ACTION_DREAMING_STOPPED: addHotspotsIfUnlocked(); break; case Intent.ACTION_SCREEN_ON: // show our hotspot in case !mKeyguardManager.isKeyguardLocked() // clear previous animations because they can interfere mSidebar.clearAnimation(); View v = mSidebar.findViewById(R.id.sidebar_container); Animation a = v.getAnimation(); if (a != null) { a.setAnimationListener(null); } v.clearAnimation(); updateNotificationStatus(); addHotspotsIfUnlocked(); break; case Intent.ACTION_CLOSE_SYSTEM_DIALOGS: onCloseSidebar(); break; case Intent.ACTION_SCREEN_OFF: onCloseSidebar(); // remove our hotspot removeHotspots(); stopForeground(true); break; } } } class CloseCallbackImpl implements CloseCallback { @Override public void close() { onCloseSidebar(); } @Override public void closeDelayed(int delayMillis) { mHandler.sendEmptyMessageDelayed(MESSAGE_CLOSE_SIDEBAR, delayMillis); } } }