diff --git a/.gitignore b/.gitignore index 34eb2a9..899f4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ gen/ local.properties .DS_Store + +## Directory-based project format: +.idea/ +integrationExample/.idea/ + +*.iml \ No newline at end of file diff --git a/integrationExample/.gitignore b/integrationExample/.gitignore new file mode 100644 index 0000000..9c4de58 --- /dev/null +++ b/integrationExample/.gitignore @@ -0,0 +1,7 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/integrationExample/.idea/.name b/integrationExample/.idea/.name new file mode 100644 index 0000000..18923a6 --- /dev/null +++ b/integrationExample/.idea/.name @@ -0,0 +1 @@ +FloatingWindowExample \ No newline at end of file diff --git a/integrationExample/.idea/compiler.xml b/integrationExample/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/integrationExample/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/copyright/profiles_settings.xml b/integrationExample/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/integrationExample/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/integrationExample/.idea/encodings.xml b/integrationExample/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/integrationExample/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/gradle.xml b/integrationExample/.idea/gradle.xml new file mode 100644 index 0000000..f1cec91 --- /dev/null +++ b/integrationExample/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/misc.xml b/integrationExample/.idea/misc.xml new file mode 100644 index 0000000..7158618 --- /dev/null +++ b/integrationExample/.idea/misc.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/modules.xml b/integrationExample/.idea/modules.xml new file mode 100644 index 0000000..8ce3d52 --- /dev/null +++ b/integrationExample/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/runConfigurations.xml b/integrationExample/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/integrationExample/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/integrationExample/.idea/vcs.xml b/integrationExample/.idea/vcs.xml new file mode 100644 index 0000000..6564d52 --- /dev/null +++ b/integrationExample/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/integrationExample/FloatingWindowExample.iml b/integrationExample/FloatingWindowExample.iml new file mode 100644 index 0000000..6b2b0df --- /dev/null +++ b/integrationExample/FloatingWindowExample.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/.gitignore b/integrationExample/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/integrationExample/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/integrationExample/app/app.iml b/integrationExample/app/app.iml new file mode 100644 index 0000000..758e867 --- /dev/null +++ b/integrationExample/app/app.iml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/build.gradle b/integrationExample/app/build.gradle new file mode 100644 index 0000000..a0df6cd --- /dev/null +++ b/integrationExample/app/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "net.alexandroid.floatingwindowexample" + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:design:23.1.1' +} diff --git a/integrationExample/app/proguard-rules.pro b/integrationExample/app/proguard-rules.pro new file mode 100644 index 0000000..1af3f1e --- /dev/null +++ b/integrationExample/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\JavaProjects\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/integrationExample/app/src/androidTest/java/net/alexandroid/floatingwindowexample/ApplicationTest.java b/integrationExample/app/src/androidTest/java/net/alexandroid/floatingwindowexample/ApplicationTest.java new file mode 100644 index 0000000..8f1d922 --- /dev/null +++ b/integrationExample/app/src/androidTest/java/net/alexandroid/floatingwindowexample/ApplicationTest.java @@ -0,0 +1,13 @@ +package net.alexandroid.floatingwindowexample; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/integrationExample/app/src/main/AndroidManifest.xml b/integrationExample/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..af4e671 --- /dev/null +++ b/integrationExample/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MainActivity.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MainActivity.java new file mode 100644 index 0000000..6a0a8e6 --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MainActivity.java @@ -0,0 +1,51 @@ +package net.alexandroid.floatingwindowexample; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(new Intent(MainActivity.this, StandOutExampleActivity.class)); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MostBasicWindow.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MostBasicWindow.java new file mode 100644 index 0000000..ee05c9a --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MostBasicWindow.java @@ -0,0 +1,35 @@ +package net.alexandroid.floatingwindowexample; + +import android.graphics.Color; +import android.widget.FrameLayout; +import android.widget.TextView; + +import wei.mark.standout.StandOutWindow; +import wei.mark.standout.ui.Window; + +public class MostBasicWindow extends StandOutWindow { + + @Override + public String getAppName() { + return "MostBasicWindow"; + } + + @Override + public int getAppIcon() { + return android.R.drawable.btn_star; + } + + @Override + public void createAndAttachView(int id, FrameLayout frame) { + TextView view = new TextView(this); + view.setText("MostBasicWindow"); + view.setBackgroundColor(Color.CYAN); + + frame.addView(view); + } + + @Override + public StandOutLayoutParams getParams(int id, Window window) { + return new StandOutLayoutParams(id, 200, 150, 100, 100); + } +} diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MultiWindow.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MultiWindow.java new file mode 100644 index 0000000..ed5d384 --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/MultiWindow.java @@ -0,0 +1,194 @@ +package net.alexandroid.floatingwindowexample; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import wei.mark.standout.StandOutWindow; +import wei.mark.standout.constants.StandOutFlags; +import wei.mark.standout.ui.Window; + +/** + * This implementation provides multiple windows. You may extend this class or + * use it as a reference for a basic foundation for your own windows. + *

+ *

+ * Functionality includes system window decorators, moveable, resizeable, + * hideable, closeable, and bring-to-frontable. + *

+ *

+ * The persistent notification creates new windows. The hidden notifications + * restores previously hidden windows. + * + * @author Mark Wei + */ +public class MultiWindow extends StandOutWindow { + + @Override + public String getAppName() { + return "MultiWindow"; + } + + @Override + public int getAppIcon() { + return android.R.drawable.ic_menu_add; + } + + @Override + public String getTitle(int id) { + return getAppName() + " " + id; + } + + @Override + public void createAndAttachView(int id, FrameLayout frame) { + // create a new layout from body.xml + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.body, frame, true); + + TextView idText = (TextView) view.findViewById(R.id.id); + idText.setText(String.valueOf(id)); + } + + // every window is initially same size + @Override + public StandOutLayoutParams getParams(int id, Window window) { + return new StandOutLayoutParams(id, 400, 300, + StandOutLayoutParams.AUTO_POSITION, + StandOutLayoutParams.AUTO_POSITION, 100, 100); + } + + // we want the system window decorations, we want to drag the body, we want + // the ability to hide windows, and we want to tap the window to bring to + // front + @Override + public int getFlags(int id) { + return StandOutFlags.FLAG_DECORATION_SYSTEM + | StandOutFlags.FLAG_BODY_MOVE_ENABLE + | StandOutFlags.FLAG_WINDOW_HIDE_ENABLE + | StandOutFlags.FLAG_WINDOW_BRING_TO_FRONT_ON_TAP + | StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE + | StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE; + } + + @Override + public String getPersistentNotificationTitle(int id) { + return getAppName() + " Running"; + } + + @Override + public String getPersistentNotificationMessage(int id) { + return "Click to add a new " + getAppName(); + } + + // return an Intent that creates a new MultiWindow + @Override + public Intent getPersistentNotificationIntent(int id) { + return StandOutWindow.getShowIntent(this, getClass(), getUniqueId()); + } + + @Override + public int getHiddenIcon() { + return android.R.drawable.ic_menu_info_details; + } + + @Override + public String getHiddenNotificationTitle(int id) { + return getAppName() + " Hidden"; + } + + @Override + public String getHiddenNotificationMessage(int id) { + return "Click to restore #" + id; + } + + // return an Intent that restores the MultiWindow + @Override + public Intent getHiddenNotificationIntent(int id) { + return StandOutWindow.getShowIntent(this, getClass(), id); + } + + @Override + public Animation getShowAnimation(int id) { + if (isExistingId(id)) { + // restore + return AnimationUtils.loadAnimation(this, + android.R.anim.slide_in_left); + } else { + // show + return super.getShowAnimation(id); + } + } + + @Override + public Animation getHideAnimation(int id) { + return AnimationUtils.loadAnimation(this, + android.R.anim.slide_out_right); + } + + @Override + public List getDropDownItems(int id) { + List items = new ArrayList(); + items.add(new DropDownListItem(android.R.drawable.ic_menu_help, + "About", new Runnable() { + + @Override + public void run() { + Toast.makeText( + MultiWindow.this, + getAppName() + + " is a demonstration of StandOut.", + Toast.LENGTH_SHORT).show(); + } + })); + items.add(new DropDownListItem(android.R.drawable.ic_menu_preferences, + "Settings", new Runnable() { + + @Override + public void run() { + Toast.makeText(MultiWindow.this, + "There are no settings.", Toast.LENGTH_SHORT) + .show(); + } + })); + return items; + } + + @Override + public void onReceiveData(int id, int requestCode, Bundle data, + Class fromCls, int fromId) { + // receive data from WidgetsWindow's button press + // to show off the data sending framework + switch (requestCode) { + case WidgetsWindow.DATA_CHANGED_TEXT: + Window window = getWindow(id); + if (window == null) { + String errorText = String.format(Locale.US, + "%s received data but Window id: %d is not open.", + getAppName(), id); + Toast.makeText(this, errorText, Toast.LENGTH_SHORT).show(); + return; + } + String changedText = data.getString("changedText"); + TextView status = (TextView) window.findViewById(R.id.id); + status.setTextSize(20); + status.setText("Received data from WidgetsWindow: " + + changedText); + break; + default: + Log.d("MultiWindow", "Unexpected data received."); + break; + } + } +} diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/SimpleWindow.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/SimpleWindow.java new file mode 100644 index 0000000..638838c --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/SimpleWindow.java @@ -0,0 +1,52 @@ +package net.alexandroid.floatingwindowexample; + +import wei.mark.standout.StandOutWindow; +import wei.mark.standout.constants.StandOutFlags; +import wei.mark.standout.ui.Window; +import android.content.Intent; +import android.view.LayoutInflater; +import android.widget.FrameLayout; + +public class SimpleWindow extends StandOutWindow { + + @Override + public String getAppName() { + return "SimpleWindow"; + } + + @Override + public int getAppIcon() { + return android.R.drawable.ic_menu_close_clear_cancel; + } + + @Override + public void createAndAttachView(int id, FrameLayout frame) { + // create a new layout from body.xml + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.simple, frame, true); + } + + // the window will be centered + @Override + public StandOutLayoutParams getParams(int id, Window window) { + return new StandOutLayoutParams(id, 250, 300, + StandOutLayoutParams.CENTER, StandOutLayoutParams.CENTER); + } + + // move the window by dragging the view + @Override + public int getFlags(int id) { + return super.getFlags(id) | StandOutFlags.FLAG_BODY_MOVE_ENABLE + | StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE; + } + + @Override + public String getPersistentNotificationMessage(int id) { + return "Click to close the SimpleWindow"; + } + + @Override + public Intent getPersistentNotificationIntent(int id) { + return StandOutWindow.getCloseIntent(this, SimpleWindow.class, id); + } +} diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/StandOutExampleActivity.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/StandOutExampleActivity.java new file mode 100644 index 0000000..ecf5d18 --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/StandOutExampleActivity.java @@ -0,0 +1,35 @@ +package net.alexandroid.floatingwindowexample; + +import android.app.Activity; +import android.os.Bundle; + +import wei.mark.standout.StandOutWindow; + +public class StandOutExampleActivity extends Activity { + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + StandOutWindow.closeAll(this, SimpleWindow.class); + StandOutWindow.closeAll(this, MultiWindow.class); + StandOutWindow.closeAll(this, WidgetsWindow.class); + + // show a MultiWindow, SimpleWindow + + StandOutWindow.show(this, SimpleWindow.class, StandOutWindow.DEFAULT_ID); + StandOutWindow.show(this, MultiWindow.class, StandOutWindow.DEFAULT_ID); + StandOutWindow.show(this, WidgetsWindow.class, + StandOutWindow.DEFAULT_ID); + + // show a MostBasicWindow. It is commented out because it does not + // support closing. + + /* + * StandOutWindow.show(this, StandOutMostBasicWindow.class, + * StandOutWindow.DEFAULT_ID); + */ + + finish(); + } +} \ No newline at end of file diff --git a/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/WidgetsWindow.java b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/WidgetsWindow.java new file mode 100644 index 0000000..3c297e0 --- /dev/null +++ b/integrationExample/app/src/main/java/net/alexandroid/floatingwindowexample/WidgetsWindow.java @@ -0,0 +1,66 @@ +package net.alexandroid.floatingwindowexample; + +import wei.mark.standout.ui.Window; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +public class WidgetsWindow extends MultiWindow { + public static final int DATA_CHANGED_TEXT = 0; + + @Override + public void createAndAttachView(final int id, FrameLayout frame) { + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.widgets, frame, true); + + final TextView status = (TextView) view.findViewById(R.id.status); + final EditText edit = (EditText) view.findViewById(R.id.edit); + final EditText edit2 = (EditText) view.findViewById(R.id.edit2); + Button button = (Button) view.findViewById(R.id.button); + button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + String text = edit.getText().toString(); + String text2 = edit2.getText().toString(); + + String changedText = "Entered: " + + text + + (text.length() == 0 || text2.length() == 0 ? "" + : " and ") + text2; + + status.setText(changedText); + edit.setText(""); + edit2.setText(""); + + // update MultiWindow:0 when button is pressed + // to show off the data sending framework + Bundle data = new Bundle(); + data.putString("changedText", changedText); + sendData(id, MultiWindow.class, DEFAULT_ID, DATA_CHANGED_TEXT, + data); + } + }); + } + + @Override + public StandOutLayoutParams getParams(int id, Window window) { + return new StandOutLayoutParams(id, 300, 500, + StandOutLayoutParams.RIGHT, StandOutLayoutParams.BOTTOM); + } + + @Override + public String getAppName() { + return "WidgetWindow"; + } + + @Override + public int getThemeStyle() { + return android.R.style.Theme_Light; + } +} \ No newline at end of file diff --git a/integrationExample/app/src/main/java/wei/mark/standout/StandOutWindow.java b/integrationExample/app/src/main/java/wei/mark/standout/StandOutWindow.java new file mode 100644 index 0000000..ecbc25b --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/StandOutWindow.java @@ -0,0 +1,1895 @@ +package wei.mark.standout; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v7.app.NotificationCompat; +import android.util.Log; +import android.view.Display; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.TextView; + +import net.alexandroid.floatingwindowexample.R; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import wei.mark.standout.constants.StandOutFlags; +import wei.mark.standout.ui.Window; + +/** + * Extend this class to easily create and manage floating StandOut windows. + * + * @author Mark Wei + *

+ * Contributors: Jason + */ +public abstract class StandOutWindow extends Service { + static final String TAG = "StandOutWindow"; + + /** + * StandOut window id: You may use this sample id for your first window. + */ + public static final int DEFAULT_ID = 0; + + /** + * Special StandOut window id: You may NOT use this id for any windows. + */ + public static final int ONGOING_NOTIFICATION_ID = -1; + + /** + * StandOut window id: You may use this id when you want it to be + * disregarded. The system makes no distinction for this id; it is only used + * to improve code readability. + */ + public static final int DISREGARD_ID = -2; + + /** + * Intent action: Show a new window corresponding to the id. + */ + public static final String ACTION_SHOW = "SHOW"; + + /** + * Intent action: Restore a previously hidden window corresponding to the + * id. The window should be previously hidden with {@link #ACTION_HIDE}. + */ + public static final String ACTION_RESTORE = "RESTORE"; + + /** + * Intent action: Close an existing window with an existing id. + */ + public static final String ACTION_CLOSE = "CLOSE"; + + /** + * Intent action: Close all existing windows. + */ + public static final String ACTION_CLOSE_ALL = "CLOSE_ALL"; + + /** + * Intent action: Send data to a new or existing window. + */ + public static final String ACTION_SEND_DATA = "SEND_DATA"; + + /** + * Intent action: Hide an existing window with an existing id. To enable the + * ability to restore this window, make sure you implement + * {@link #getHiddenNotification(int)}. + */ + public static final String ACTION_HIDE = "HIDE"; + + /** + * Show a new window corresponding to the id, or restore a previously hidden + * window. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that will be used + * to create and manage the window. + * @param id The id representing this window. If the id exists, and the + * corresponding window was previously hidden, then that window + * will be restored. + * @see #show(int) + */ + public static void show(Context context, + Class cls, int id) { + context.startService(getShowIntent(context, cls, id)); + } + + /** + * Hide the existing window corresponding to the id. To enable the ability + * to restore this window, make sure you implement + * {@link #getHiddenNotification(int)}. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @param id The id representing this window. The window must previously be + * shown. + * @see #hide(int) + */ + public static void hide(Context context, + Class cls, int id) { + context.startService(getHideIntent(context, cls, id)); + } + + /** + * Close an existing window with an existing id. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @param id The id representing this window. The window must previously be + * shown. + * @see #close(int) + */ + public static void close(Context context, + Class cls, int id) { + context.startService(getCloseIntent(context, cls, id)); + } + + /** + * Close all existing windows. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @see #closeAll() + */ + public static void closeAll(Context context, + Class cls) { + context.startService(getCloseAllIntent(context, cls)); + } + + /** + * This allows windows of different applications to communicate with each + * other. + *

+ *

+ * Send {} data in a {@link Bundle} to a new or existing + * windows. The implementation of the recipient window can handle what to do + * with the data. To receive a result, provide the class and id of the + * sender. + * + * @param context A Context of the application package implementing the class of + * the sending window. + * @param toCls The Service's class extending {@link StandOutWindow} that is + * managing the receiving window. + * @param toId The id of the receiving window, or DISREGARD_ID. + * @param requestCode Provide a request code to declare what kind of data is being + * sent. + * @param data A bundle of parceleable data to be sent to the receiving + * window. + * @param fromCls Provide the class of the sending window if you want a result. + * @param fromId Provide the id of the sending window if you want a result. + * @see #sendData(int, Class, int, int, Bundle) + */ + public static void sendData(Context context, + Class toCls, int toId, int requestCode, + Bundle data, Class fromCls, int fromId) { + context.startService(getSendDataIntent(context, toCls, toId, + requestCode, data, fromCls, fromId)); + } + + /** + * See {@link #show(Context, Class, int)}. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that will be used + * to create and manage the window. + * @param id The id representing this window. If the id exists, and the + * corresponding window was previously hidden, then that window + * will be restored. + * @return An {@link Intent} to use with + * {@link Context#startService(Intent)}. + */ + public static Intent getShowIntent(Context context, + Class cls, int id) { + boolean cached = sWindowCache.isCached(id, cls); + String action = cached ? ACTION_RESTORE : ACTION_SHOW; + Uri uri = cached ? Uri.parse("standout://" + cls + '/' + id) : null; + return new Intent(context, cls).putExtra("id", id).setAction(action) + .setData(uri); + } + + /** + * See {@link #hide(Context, Class, int)}. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @param id The id representing this window. If the id exists, and the + * corresponding window was previously hidden, then that window + * will be restored. + * @return An {@link Intent} to use with + * {@link Context#startService(Intent)}. + */ + public static Intent getHideIntent(Context context, + Class cls, int id) { + return new Intent(context, cls).putExtra("id", id).setAction( + ACTION_HIDE); + } + + /** + * See {@link #close(Context, Class, int)}. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @param id The id representing this window. If the id exists, and the + * corresponding window was previously hidden, then that window + * will be restored. + * @return An {@link Intent} to use with + * {@link Context#startService(Intent)}. + */ + public static Intent getCloseIntent(Context context, + Class cls, int id) { + return new Intent(context, cls).putExtra("id", id).setAction( + ACTION_CLOSE); + } + + /** + * See { #closeAll(Context, Class, int)}. + * + * @param context A Context of the application package implementing this class. + * @param cls The Service extending {@link StandOutWindow} that is managing + * the window. + * @return An {@link Intent} to use with + * {@link Context#startService(Intent)}. + */ + public static Intent getCloseAllIntent(Context context, + Class cls) { + return new Intent(context, cls).setAction(ACTION_CLOSE_ALL); + } + + /** + * See {@link #sendData(Context, Class, int, int, Bundle, Class, int)}. + * + * @param context A Context of the application package implementing the class of + * the sending window. + * @param toCls The Service's class extending {@link StandOutWindow} that is + * managing the receiving window. + * @param toId The id of the receiving window. + * @param requestCode Provide a request code to declare what kind of data is being + * sent. + * @param data A bundle of parceleable data to be sent to the receiving + * window. + * @param fromCls If the sending window wants a result, provide the class of the + * sending window. + * @param fromId If the sending window wants a result, provide the id of the + * sending window. + * @return An {@link } to use with + * {@link Context#startService(Intent)}. + */ + public static Intent getSendDataIntent(Context context, + Class toCls, int toId, int requestCode, + Bundle data, Class fromCls, int fromId) { + return new Intent(context, toCls).putExtra("id", toId) + .putExtra("requestCode", requestCode) + .putExtra("wei.mark.standout.data", data) + .putExtra("wei.mark.standout.fromCls", fromCls) + .putExtra("fromId", fromId).setAction(ACTION_SEND_DATA); + } + + // internal map of ids to shown/hidden views + static WindowCache sWindowCache; + static Window sFocusedWindow; + + // static constructors + static { + sWindowCache = new WindowCache(); + sFocusedWindow = null; + } + + // internal system services + WindowManager mWindowManager; + private NotificationManager mNotificationManager; + LayoutInflater mLayoutInflater; + + // internal state variables + private boolean startedForeground; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + startedForeground = false; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + // intent should be created with + // getShowIntent(), getHideIntent(), getCloseIntent() + if (intent != null) { + String action = intent.getAction(); + int id = intent.getIntExtra("id", DEFAULT_ID); + + // this will interfere with getPersistentNotification() + if (id == ONGOING_NOTIFICATION_ID) { + throw new RuntimeException( + "ID cannot equals StandOutWindow.ONGOING_NOTIFICATION_ID"); + } + + if (ACTION_SHOW.equals(action) || ACTION_RESTORE.equals(action)) { + show(id); + } else if (ACTION_HIDE.equals(action)) { + hide(id); + } else if (ACTION_CLOSE.equals(action)) { + close(id); + } else if (ACTION_CLOSE_ALL.equals(action)) { + closeAll(); + } else if (ACTION_SEND_DATA.equals(action)) { + if (!isExistingId(id) && id != DISREGARD_ID) { + Log.w(TAG, + "Sending data to non-existant window. If this is not intended, make sure toId is either an existing window's id or DISREGARD_ID."); + } + Bundle data = intent.getBundleExtra("wei.mark.standout.data"); + int requestCode = intent.getIntExtra("requestCode", 0); + @SuppressWarnings("unchecked") + Class fromCls = (Class) intent + .getSerializableExtra("wei.mark.standout.fromCls"); + int fromId = intent.getIntExtra("fromId", DEFAULT_ID); + onReceiveData(id, requestCode, data, fromCls, fromId); + } + } else { + Log.w(TAG, "Tried to onStartCommand() with a null intent."); + } + + // the service is started in foreground in show() + // so we don't expect Android to kill this service + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // closes all windows + closeAll(); + } + + /** + * Return the name of every window in this implementation. The name will + * appear in the default implementations of the system window decoration + * title and notification titles. + * + * @return The name. + */ + public abstract String getAppName(); + + /** + * Return the icon resource for every window in this implementation. The + * icon will appear in the default implementations of the system window + * decoration and notifications. + * + * @return The icon. + */ + public abstract int getAppIcon(); + + /** + * Create a new {@link View} corresponding to the id, and add it as a child + * to the frame. The view will become the contents of this StandOut window. + * The view MUST be newly created, and you MUST attach it to the frame. + *

+ *

+ * If you are inflating your view from XML, make sure you use + * {@link LayoutInflater#inflate(int, ViewGroup, boolean)} to attach your + * view to frame. Set the ViewGroup to be frame, and the boolean to true. + *

+ *

+ * If you are creating your view programmatically, make sure you use + * {@link FrameLayout#addView(View)} to add your view to the frame. + * + * @param id The id representing the window. + * @param frame The {@link FrameLayout} to attach your view as a child to. + */ + public abstract void createAndAttachView(int id, FrameLayout frame); + + /** + * Return the {@link StandOutWindow#} for the corresponding id. + * The system will set the layout params on the view for this StandOut + * window. The layout params may be reused. + * + * @param id The id of the window. + * @param window The window corresponding to the id. Given as courtesy, so you + * may get the existing layout params. + * @return The {@link StandOutWindow#} corresponding to the id. + * The layout params will be set on the window. The layout params + * returned will be reused whenever possible, minimizing the number + * of times getParams() will be called. + */ + public abstract StandOutLayoutParams getParams(int id, Window window); + + /** + * Implement this method to change modify the behavior and appearance of the + * window corresponding to the id. + *

+ *

+ * You may use any of the flags defined in {@link StandOutFlags}. This + * method will be called many times, so keep it fast. + *

+ *

+ * Use bitwise OR (|) to set flags, and bitwise XOR (^) to unset flags. To + * test if a flag is set, use {@link Utils#isSet(int, int)}. + * + * @param id The id of the window. + * @return A combination of flags. + */ + public int getFlags(int id) { + return 0; + } + + /** + * Implement this method to set a custom title for the window corresponding + * to the id. + * + * @param id The id of the window. + * @return The title of the window. + */ + public String getTitle(int id) { + return getAppName(); + } + + /** + * Implement this method to set a custom icon for the window corresponding + * to the id. + * + * @param id The id of the window. + * @return The icon of the window. + */ + public int getIcon(int id) { + return getAppIcon(); + } + + /** + * Return the title for the persistent notification. This is called every + * time {@link #show(int)} is called. + * + * @param id The id of the window shown. + * @return The title for the persistent notification. + */ + public String getPersistentNotificationTitle(int id) { + return getAppName() + " Running"; + } + + /** + * Return the message for the persistent notification. This is called every + * time {@link #show(int)} is called. + * + * @param id The id of the window shown. + * @return The message for the persistent notification. + */ + public String getPersistentNotificationMessage(int id) { + return ""; + } + + /** + * Return the intent for the persistent notification. This is called every + * time {@link #show(int)} is called. + *

+ *

+ * The returned intent will be packaged into a {@link PendingIntent} to be + * invoked when the user clicks the notification. + * + * @param id The id of the window shown. + * @return The intent for the persistent notification. + */ + public Intent getPersistentNotificationIntent(int id) { + return null; + } + + /** + * Return the icon resource for every hidden window in this implementation. + * The icon will appear in the default implementations of the hidden + * notifications. + * + * @return The icon. + */ + public int getHiddenIcon() { + return getAppIcon(); + } + + /** + * Return the title for the hidden notification corresponding to the window + * being hidden. + * + * @param id The id of the hidden window. + * @return The title for the hidden notification. + */ + public String getHiddenNotificationTitle(int id) { + return getAppName() + " Hidden"; + } + + /** + * Return the message for the hidden notification corresponding to the + * window being hidden. + * + * @param id The id of the hidden window. + * @return The message for the hidden notification. + */ + public String getHiddenNotificationMessage(int id) { + return ""; + } + + /** + * Return the intent for the hidden notification corresponding to the window + * being hidden. + *

+ *

+ * The returned intent will be packaged into a {@link PendingIntent} to be + * invoked when the user clicks the notification. + * + * @param id The id of the hidden window. + * @return The intent for the hidden notification. + */ + public Intent getHiddenNotificationIntent(int id) { + return null; + } + + /** + * Return a persistent {@link Notification} for the corresponding id. You + * must return a notification for AT LEAST the first id to be requested. + * Once the persistent notification is shown, further calls to + * {@link #getPersistentNotification(int)} may return null. This way Android + * can start the StandOut window service in the foreground and will not kill + * the service on low memory. + *

+ *

+ * As a courtesy, the system will request a notification for every new id + * shown. Your implementation is encouraged to include the + * {@link PendingIntent#FLAG_UPDATE_CURRENT} flag in the notification so + * that there is only one system-wide persistent notification. + *

+ *

+ * See the StandOutExample project for an implementation of + * {@link #getPersistentNotification(int)} that keeps one system-wide + * persistent notification that creates a new window on every click. + * + * @param id The id of the window. + * @return The {@link Notification} corresponding to the id, or null if + * you've previously returned a notification. + */ + public Notification getPersistentNotification(int id) { + // basic notification stuff + // http://developer.android.com/guide/topics/ui/notifiers/notifications.html + int icon = getAppIcon(); + long when = System.currentTimeMillis(); + Context c = getApplicationContext(); + String contentTitle = getPersistentNotificationTitle(id); + String contentText = getPersistentNotificationMessage(id); + String tickerText = String.format("%s: %s", contentTitle, contentText); + + // getPersistentNotification() is called for every new window + // so we replace the old notification with a new one that has + // a bigger id + Intent notificationIntent = getPersistentNotificationIntent(id); + + PendingIntent contentIntent = null; + + if (notificationIntent != null) { + contentIntent = PendingIntent.getService(this, 0, + notificationIntent, + // flag updates existing persistent notification + PendingIntent.FLAG_UPDATE_CURRENT); + } + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + + return builder + .setSmallIcon(icon) + .setTicker(tickerText) + .setWhen(when) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentIntent(contentIntent) + .build(); + } + + /** + * Return a hidden {@link Notification} for the corresponding id. The system + * will request a notification for every id that is hidden. + *

+ *

+ * If null is returned, StandOut will assume you do not wish to support + * hiding this window, and will {@link #close(int)} it for you. + *

+ *

+ * See the StandOutExample project for an implementation of + * {@link #getHiddenNotification(int)} that for every hidden window keeps a + * notification which restores that window upon user's click. + * + * @param id The id of the window. + * @return The {@link Notification} corresponding to the id or null. + */ + public Notification getHiddenNotification(int id) { + // same basics as getPersistentNotification() + int icon = getHiddenIcon(); + long when = System.currentTimeMillis(); + Context c = getApplicationContext(); + String contentTitle = getHiddenNotificationTitle(id); + String contentText = getHiddenNotificationMessage(id); + String tickerText = String.format("%s: %s", contentTitle, contentText); + + // the difference here is we are providing the same id + Intent notificationIntent = getHiddenNotificationIntent(id); + + PendingIntent contentIntent = null; + + if (notificationIntent != null) { + contentIntent = PendingIntent.getService(this, 0, + notificationIntent, + // flag updates existing persistent notification + PendingIntent.FLAG_UPDATE_CURRENT); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + + return builder + .setSmallIcon(icon) + .setTicker(tickerText) + .setWhen(when) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentIntent(contentIntent) + .build(); + } + + /** + * Return the animation to play when the window corresponding to the id is + * shown. + * + * @param id The id of the window. + * @return The animation to play or null. + */ + public Animation getShowAnimation(int id) { + return AnimationUtils.loadAnimation(this, android.R.anim.fade_in); + } + + /** + * Return the animation to play when the window corresponding to the id is + * hidden. + * + * @param id The id of the window. + * @return The animation to play or null. + */ + public Animation getHideAnimation(int id) { + return AnimationUtils.loadAnimation(this, android.R.anim.fade_out); + } + + /** + * Return the animation to play when the window corresponding to the id is + * closed. + * + * @param id The id of the window. + * @return The animation to play or null. + */ + public Animation getCloseAnimation(int id) { + return AnimationUtils.loadAnimation(this, android.R.anim.fade_out); + } + + /** + * Implement this method to set a custom theme for all windows in this + * implementation. + * + * @return The theme to set on the window, or 0 for device default. + */ + public int getThemeStyle() { + return 0; + } + + /** + * You probably want to leave this method alone and implement + * {@link #getDropDownItems(int)} instead. Only implement this method if you + * want more control over the drop down menu. + *

+ *

+ * Implement this method to set a custom drop down menu when the user clicks + * on the icon of the window corresponding to the id. The icon is only shown + * when {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set. + * + * @param id The id of the window. + * @return The drop down menu to be anchored to the icon, or null to have no + * dropdown menu. + */ + public PopupWindow getDropDown(final int id) { + final List items; + + List dropDownListItems = getDropDownItems(id); + if (dropDownListItems != null) { + items = dropDownListItems; + } else { + items = new ArrayList(); + } + + // add default drop down items + items.add(new DropDownListItem( + android.R.drawable.ic_menu_close_clear_cancel, "Quit " + + getAppName(), new Runnable() { + + @Override + public void run() { + closeAll(); + } + })); + + // turn item list into views in PopupWindow + LinearLayout list = new LinearLayout(this); + list.setOrientation(LinearLayout.VERTICAL); + + final PopupWindow dropDown = new PopupWindow(list, + StandOutLayoutParams.WRAP_CONTENT, + StandOutLayoutParams.WRAP_CONTENT, true); + + for (final DropDownListItem item : items) { + ViewGroup listItem = (ViewGroup) mLayoutInflater.inflate( + R.layout.drop_down_list_item, null); + list.addView(listItem); + + ImageView icon = (ImageView) listItem.findViewById(R.id.icon); + icon.setImageResource(item.icon); + + TextView description = (TextView) listItem + .findViewById(R.id.description); + description.setText(item.description); + + listItem.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + item.action.run(); + dropDown.dismiss(); + } + }); + } + + Drawable background = getResources().getDrawable( + android.R.drawable.editbox_dropdown_dark_frame); + dropDown.setBackgroundDrawable(background); + return dropDown; + } + + /** + * Implement this method to populate the drop down menu when the user clicks + * on the icon of the window corresponding to the id. The icon is only shown + * when {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set. + * + * @param id The id of the window. + * @return The list of items to show in the drop down menu, or null or empty + * to have no dropdown menu. + */ + public List getDropDownItems(int id) { + return null; + } + + /** + * Implement this method to be alerted to touch events in the body of the + * window corresponding to the id. + *

+ *

+ * Note that even if you set {@link #FLAG_DECORATION_SYSTEM}, you will not + * receive touch events from the system window decorations. + * + * @param id The id of the view, provided as a courtesy. + * @param window The window corresponding to the id, provided as a courtesy. + * @param view The view where the event originated from. + * @param event See linked method. + * @see {@link View.OnTouchListener#onTouch(View, MotionEvent)} + */ + public boolean onTouchBody(int id, Window window, View view, + MotionEvent event) { + return false; + } + + /** + * Implement this method to be alerted to when the window corresponding to + * the id is moved. + * + * @param id The id of the view, provided as a courtesy. + * @param window The window corresponding to the id, provided as a courtesy. + * @param view The view where the event originated from. + * @param event See linked method. + * @see {@link #onTouchHandleMove(int, Window, View, MotionEvent)} + */ + public void onMove(int id, Window window, View view, MotionEvent event) { + } + + /** + * Implement this method to be alerted to when the window corresponding to + * the id is resized. + * + * @param id The id of the view, provided as a courtesy. + * @param window The window corresponding to the id, provided as a courtesy. + * @param view The view where the event originated from. + * @param event See linked method. + * @see {@link #onTouchHandleResize(int, Window, View, MotionEvent)} + */ + public void onResize(int id, Window window, View view, MotionEvent event) { + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to be shown. This callback will occur before the view is + * added to the window manager. + * + * @param id The id of the view, provided as a courtesy. + * @param view The view about to be shown. + * @return Return true to cancel the view from being shown, or false to + * continue. + * @see #show(int) + */ + public boolean onShow(int id, Window window) { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to be hidden. This callback will occur before the view is + * removed from the window manager and {@link #getHiddenNotification(int)} + * is called. + * + * @param id The id of the view, provided as a courtesy. + * @param view The view about to be hidden. + * @return Return true to cancel the view from being hidden, or false to + * continue. + * @see #hide(int) + */ + public boolean onHide(int id, Window window) { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to be closed. This callback will occur before the view is + * removed from the window manager. + * + * @param id The id of the view, provided as a courtesy. + * @param view The view about to be closed. + * @return Return true to cancel the view from being closed, or false to + * continue. + * @see #close(int) + */ + public boolean onClose(int id, Window window) { + return false; + } + + /** + * Implement this callback to be alerted when all windows are about to be + * closed. This callback will occur before any views are removed from the + * window manager. + * + * @return Return true to cancel the views from being closed, or false to + * continue. + * @see #closeAll() + */ + public boolean onCloseAll() { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id has received some data. The sender is described by fromCls and fromId + * if the sender wants a result. To send a result, use + * {@link #sendData(int, Class, int, int, Bundle)}. + * + * @param id The id of your receiving window. + * @param requestCode The sending window provided this request code to declare what + * kind of data is being sent. + * @param data A bundle of parceleable data that was sent to your receiving + * window. + * @param fromCls The sending window's class. Provided if the sender wants a + * result. + * @param fromId The sending window's id. Provided if the sender wants a + * result. + */ + public void onReceiveData(int id, int requestCode, Bundle data, + Class fromCls, int fromId) { + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to be updated in the layout. This callback will occur before + * the view is updated by the window manager. + * + * @param id The id of the window, provided as a courtesy. + * @param view The window about to be updated. + * @param params The updated layout params. + * @return Return true to cancel the window from being updated, or false to + * continue. + * @see #updateViewLayout(int, Window, StandOutLayoutParams) + */ + public boolean onUpdate(int id, Window window, StandOutLayoutParams params) { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to be bought to the front. This callback will occur before + * the window is brought to the front by the window manager. + * + * @param id The id of the window, provided as a courtesy. + * @param view The window about to be brought to the front. + * @return Return true to cancel the window from being brought to the front, + * or false to continue. + * @see #bringToFront(int) + */ + public boolean onBringToFront(int id, Window window) { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id is about to have its focus changed. This callback will occur before + * the window's focus is changed. + * + * @param id The id of the window, provided as a courtesy. + * @param view The window about to be brought to the front. + * @param focus Whether the window is gaining or losing focus. + * @return Return true to cancel the window's focus from being changed, or + * false to continue. + * @see #focus(int) + */ + public boolean onFocusChange(int id, Window window, boolean focus) { + return false; + } + + /** + * Implement this callback to be alerted when a window corresponding to the + * id receives a key event. This callback will occur before the window + * handles the event with {@link Window#dispatchKeyEvent(KeyEvent)}. + * + * @param id The id of the window, provided as a courtesy. + * @param view The window about to receive the key event. + * @param event The key event. + * @return Return true to cancel the window from handling the key event, or + * false to let the window handle the key event. + * @see {@link Window#dispatchKeyEvent(KeyEvent)} + */ + public boolean onKeyEvent(int id, Window window, KeyEvent event) { + return false; + } + + /** + * Show or restore a window corresponding to the id. Return the window that + * was shown/restored. + * + * @param id The id of the window. + * @return The window shown. + */ + public final synchronized Window show(int id) { + // get the window corresponding to the id + Window cachedWindow = getWindow(id); + final Window window; + + // check cache first + if (cachedWindow != null) { + window = cachedWindow; + } else { + window = new Window(this, id); + } + + // alert callbacks and cancel if instructed + if (onShow(id, window)) { + Log.d(TAG, "Window " + id + " show cancelled by implementation."); + return null; + } + + // focus an already shown window + if (window.visibility == Window.VISIBILITY_VISIBLE) { + Log.d(TAG, "Window " + id + " is already shown."); + focus(id); + return window; + } + + window.visibility = Window.VISIBILITY_VISIBLE; + + // get animation + Animation animation = getShowAnimation(id); + + // get the params corresponding to the id + StandOutLayoutParams params = window.getLayoutParams(); + + try { + // add the view to the window manager + mWindowManager.addView(window, params); + + // animate + if (animation != null) { + window.getChildAt(0).startAnimation(animation); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + // add view to internal map + sWindowCache.putCache(id, getClass(), window); + + // get the persistent notification + Notification notification = getPersistentNotification(id); + + // show the notification + if (notification != null) { + notification.flags = notification.flags + | Notification.FLAG_NO_CLEAR; + + // only show notification if not shown before + if (!startedForeground) { + // tell Android system to show notification + startForeground( + getClass().hashCode() + ONGOING_NOTIFICATION_ID, + notification); + startedForeground = true; + } else { + // update notification if shown before + mNotificationManager.notify(getClass().hashCode() + + ONGOING_NOTIFICATION_ID, notification); + } + } else { + // notification can only be null if it was provided before + if (!startedForeground) { + throw new RuntimeException("Your StandOutWindow service must" + + "provide a persistent notification." + + "The notification prevents Android" + + "from killing your service in low" + + "memory situations."); + } + } + + focus(id); + + return window; + } + + /** + * Hide a window corresponding to the id. Show a notification for the hidden + * window. + * + * @param id The id of the window. + */ + public final synchronized void hide(int id) { + // get the view corresponding to the id + final Window window = getWindow(id); + + if (window == null) { + Log.e(TAG, "Tried to hide(" + id + ") a null window."); + return; + } + + // alert callbacks and cancel if instructed + if (onHide(id, window)) { + Log.d(TAG, "Window " + id + " hide cancelled by implementation."); + return; + } + + // ignore if window is already hidden + if (window.visibility == Window.VISIBILITY_GONE) { + Log.d(TAG, "Window " + id + " is already hidden."); + return; + } + + // check if hide enabled + if (Utils.isSet(window.flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) { + // get the hidden notification for this view + Notification notification = getHiddenNotification(id); + + // get animation + Animation animation = getHideAnimation(id); + + try { + // animate + if (animation != null) { + animation.setAnimationListener(new AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + window.visibility = Window.VISIBILITY_TRANSITION; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + // remove the window from the window manager + removeView(window); + window.visibility = Window.VISIBILITY_GONE; + } + }); + window.getChildAt(0).startAnimation(animation); + } else { + // remove the window from the window manager + removeView(window); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + if (notification != null) { + // display the notification + notification.flags = notification.flags + | Notification.FLAG_NO_CLEAR + | Notification.FLAG_AUTO_CANCEL; + + mNotificationManager.notify(getClass().hashCode() + id, + notification); + } + + } else { + // if hide not enabled, close window + close(id); + } + } + + /** + * Close a window corresponding to the id. + * + * @param id The id of the window. + */ + public final synchronized void close(final int id) { + // get the view corresponding to the id + final Window window = getWindow(id); + + if (window == null) { + Log.e(TAG, "Tried to close(" + id + ") a null window."); + return; + } + + // if hiding animation is in progress, then cancel hide animation + if (window.visibility == Window.VISIBILITY_TRANSITION) { + final Animation animation = window.getChildAt(0).getAnimation(); + if (animation != null) { + animation.setAnimationListener(null); + animation.cancel(); + window.visibility = Window.VISIBILITY_VISIBLE; + } else + window.visibility = Window.VISIBILITY_GONE; + } + + final boolean isVisible = window.visibility == Window.VISIBILITY_VISIBLE; + + // alert callbacks and cancel if instructed + if (onClose(id, window)) { + Log.w(TAG, "Window " + id + " close cancelled by implementation."); + return; + } + + // remove hidden notification + mNotificationManager.cancel(getClass().hashCode() + id); + + unfocus(window); + + // get animation + Animation animation = getCloseAnimation(id); + + // remove window + try { + // animate + if (isVisible && (animation != null)) { + animation.setAnimationListener(new AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + window.visibility = Window.VISIBILITY_TRANSITION; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + // remove the window from the window manager + removeView(window); + window.visibility = Window.VISIBILITY_GONE; + + // remove view from internal map + sWindowCache.removeCache(id, + StandOutWindow.this.getClass()); + + // if we just released the last window, quit + if (getExistingIds().size() == 0) { + // tell Android to remove the persistent + // notification + // the Service will be shutdown by the system on low + // memory + startedForeground = false; + stopForeground(true); + } + } + }); + window.getChildAt(0).startAnimation(animation); + } else { + // remove the window from the window manager + if (isVisible) + removeView(window); + + // remove view from internal map + sWindowCache.removeCache(id, getClass()); + + // if we just released the last window, quit + if (sWindowCache.getCacheSize(getClass()) == 0) { + // tell Android to remove the persistent notification + // the Service will be shutdown by the system on low memory + startedForeground = false; + stopForeground(true); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Close all existing windows. + */ + public final synchronized void closeAll() { + // alert callbacks and cancel if instructed + if (onCloseAll()) { + Log.w(TAG, "Windows close all cancelled by implementation."); + return; + } + + // add ids to temporary set to avoid concurrent modification + LinkedList ids = new LinkedList(); + for (int id : getExistingIds()) { + ids.add(id); + } + + // close each window + for (int id : ids) { + close(id); + } + } + + /** + * Send {@link Parceleable} data in a {@link Bundle} to a new or existing + * windows. The implementation of the recipient window can handle what to do + * with the data. To receive a result, provide the id of the sender. + * + * @param fromId Provide the id of the sending window if you want a result. + * @param toCls The Service's class extending {@link StandOutWindow} that is + * managing the receiving window. + * @param toId The id of the receiving window. + * @param requestCode Provide a request code to declare what kind of data is being + * sent. + * @param data A bundle of parceleable data to be sent to the receiving + * window. + */ + public final void sendData(int fromId, + Class toCls, int toId, int requestCode, + Bundle data) { + StandOutWindow.sendData(this, toCls, toId, requestCode, data, + getClass(), fromId); + } + + /** + * Bring the window corresponding to this id in front of all other windows. + * The window may flicker as it is removed and restored by the system. + * + * @param id The id of the window to bring to the front. + */ + public final synchronized void bringToFront(int id) { + Window window = getWindow(id); + if (window == null) { + Log.e(TAG, "Tried to bringToFront(" + id + ") a null window."); + return; + } + + if (window.visibility == Window.VISIBILITY_GONE) { + Log.e(TAG, "Tried to bringToFront(" + id + ") a window that is not shown."); + return; + } + + if (window.visibility == Window.VISIBILITY_TRANSITION) { + return; + } + + // alert callbacks and cancel if instructed + if (onBringToFront(id, window)) { + Log.w(TAG, "Window " + id + + " bring to front cancelled by implementation."); + return; + } + + StandOutLayoutParams params = window.getLayoutParams(); + + // remove from window manager then add back + try { + mWindowManager.removeView(window); + } catch (Exception ex) { + ex.printStackTrace(); + } + try { + mWindowManager.addView(window, params); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Request focus for the window corresponding to this id. A maximum of one + * window can have focus, and that window will receive all key events, + * including Back and Menu. + * + * @param id The id of the window. + * @return True if focus changed successfully, false if it failed. + */ + public final synchronized boolean focus(int id) { + // check if that window is focusable + final Window window = getWindow(id); + if (window == null) { + Log.e(TAG, "Tried to focus(" + id + ") a null window."); + return false; + } + + if (!Utils.isSet(window.flags, + StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) { + // remove focus from previously focused window + if (sFocusedWindow != null) { + unfocus(sFocusedWindow); + } + + return window.onFocus(true); + } + + return false; + } + + /** + * Remove focus for the window corresponding to this id. Once a window is + * unfocused, it will stop receiving key events. + * + * @param id The id of the window. + * @return True if focus changed successfully, false if it failed. + */ + public final synchronized boolean unfocus(int id) { + Window window = getWindow(id); + return unfocus(window); + } + + /** + * Courtesy method for your implementation to use if you want to. Gets a + * unique id to assign to a new window. + * + * @return The unique id. + */ + public final int getUniqueId() { + int unique = DEFAULT_ID; + for (int id : getExistingIds()) { + unique = Math.max(unique, id + 1); + } + return unique; + } + + /** + * Return whether the window corresponding to the id exists. This is useful + * for testing if the id is being restored (return true) or shown for the + * first time (return false). + * + * @param id The id of the window. + * @return True if the window corresponding to the id is either shown or + * hidden, or false if it has never been shown or was previously + * closed. + */ + public final boolean isExistingId(int id) { + return sWindowCache.isCached(id, getClass()); + } + + /** + * Return the ids of all shown or hidden windows. + * + * @return A set of ids, or an empty set. + */ + public final Set getExistingIds() { + return sWindowCache.getCacheIds(getClass()); + } + + /** + * Return the window corresponding to the id, if it exists in cache. The + * window will not be created with + * {@link #createAndAttachView(int, ViewGroup)}. This means the returned + * value will be null if the window is not shown or hidden. + * + * @param id The id of the window. + * @return The window if it is shown/hidden, or null if it is closed. + */ + public final Window getWindow(int id) { + return sWindowCache.getCache(id, getClass()); + } + + /** + * Return the window that currently has focus. + * + * @return The window that has focus. + */ + public final Window getFocusedWindow() { + return sFocusedWindow; + } + + /** + * Sets the window that currently has focus. + */ + public final void setFocusedWindow(Window window) { + sFocusedWindow = window; + } + + /** + * Remove the window from the window manager + */ + public final void removeView(Window window) { + try { + mWindowManager.removeView(window); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Change the title of the window, if such a title exists. A title exists if + * {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set, or if your own view + * contains a TextView with id R.id.title. + * + * @param id The id of the window. + * @param text The new title. + */ + public final void setTitle(int id, String text) { + Window window = getWindow(id); + if (window != null) { + View title = window.findViewById(R.id.title); + if (title instanceof TextView) { + ((TextView) title).setText(text); + } + } + } + + /** + * Change the icon of the window, if such a icon exists. A icon exists if + * {@link StandOutFlags#FLAG_DECORATION_SYSTEM} is set, or if your own view + * contains a TextView with id R.id.window_icon. + * + * @param id The id of the window. + * @param drawableRes The new icon. + */ + public final void setIcon(int id, int drawableRes) { + Window window = getWindow(id); + if (window != null) { + View icon = window.findViewById(R.id.window_icon); + if (icon instanceof ImageView) { + ((ImageView) icon).setImageResource(drawableRes); + } + } + } + + /** + * Internal touch handler for handling moving the window. + * + * @param id + * @param window + * @param view + * @param event + * @return + * @see {@link View#onTouchEvent(MotionEvent)} + */ + public boolean onTouchHandleMove(int id, Window window, View view, + MotionEvent event) { + StandOutLayoutParams params = window.getLayoutParams(); + + // how much you have to move in either direction in order for the + // gesture to be a move and not tap + + int totalDeltaX = window.touchInfo.lastX - window.touchInfo.firstX; + int totalDeltaY = window.touchInfo.lastY - window.touchInfo.firstY; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + window.touchInfo.lastX = (int) event.getRawX(); + window.touchInfo.lastY = (int) event.getRawY(); + + window.touchInfo.firstX = window.touchInfo.lastX; + window.touchInfo.firstY = window.touchInfo.lastY; + break; + case MotionEvent.ACTION_MOVE: + int deltaX = (int) event.getRawX() - window.touchInfo.lastX; + int deltaY = (int) event.getRawY() - window.touchInfo.lastY; + + window.touchInfo.lastX = (int) event.getRawX(); + window.touchInfo.lastY = (int) event.getRawY(); + + if (window.touchInfo.moving + || Math.abs(totalDeltaX) >= params.threshold + || Math.abs(totalDeltaY) >= params.threshold) { + window.touchInfo.moving = true; + + // if window is moveable + if (Utils.isSet(window.flags, + StandOutFlags.FLAG_BODY_MOVE_ENABLE)) { + + // update the position of the window + if (event.getPointerCount() == 1) { + params.x += deltaX; + params.y += deltaY; + } + + window.edit().setPosition(params.x, params.y).commit(); + } + } + break; + case MotionEvent.ACTION_UP: + window.touchInfo.moving = false; + + if (event.getPointerCount() == 1) { + + // bring to front on tap + boolean tap = Math.abs(totalDeltaX) < params.threshold + && Math.abs(totalDeltaY) < params.threshold; + if (tap + && Utils.isSet( + window.flags, + StandOutFlags.FLAG_WINDOW_BRING_TO_FRONT_ON_TAP)) { + StandOutWindow.this.bringToFront(id); + } + } + + // bring to front on touch + else if (Utils.isSet(window.flags, + StandOutFlags.FLAG_WINDOW_BRING_TO_FRONT_ON_TOUCH)) { + StandOutWindow.this.bringToFront(id); + } + + break; + } + + onMove(id, window, view, event); + + return true; + } + + /** + * Internal touch handler for handling resizing the window. + * + * @param id + * @param window + * @param view + * @param event + * @return + * @see {@link View#onTouchEvent(MotionEvent)} + */ + public boolean onTouchHandleResize(int id, Window window, View view, + MotionEvent event) { + StandOutLayoutParams params = (StandOutLayoutParams) window + .getLayoutParams(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + window.touchInfo.lastX = (int) event.getRawX(); + window.touchInfo.lastY = (int) event.getRawY(); + + window.touchInfo.firstX = window.touchInfo.lastX; + window.touchInfo.firstY = window.touchInfo.lastY; + break; + case MotionEvent.ACTION_MOVE: + int deltaX = (int) event.getRawX() - window.touchInfo.lastX; + int deltaY = (int) event.getRawY() - window.touchInfo.lastY; + + // update the size of the window + params.width += deltaX; + params.height += deltaY; + + // keep window between min/max width/height + if (params.width >= params.minWidth + && params.width <= params.maxWidth) { + window.touchInfo.lastX = (int) event.getRawX(); + } + + if (params.height >= params.minHeight + && params.height <= params.maxHeight) { + window.touchInfo.lastY = (int) event.getRawY(); + } + + window.edit().setSize(params.width, params.height).commit(); + break; + case MotionEvent.ACTION_UP: + break; + } + + onResize(id, window, view, event); + + return true; + } + + /** + * Remove focus for the window, which could belong to another application. + * Since we don't allow windows from different applications to directly + * interact with each other, except for + * {@link #sendData(Context, Class, int, int, Bundle, Class, int)}, this + * method is private. + * + * @param window The window to unfocus. + * @return True if focus changed successfully, false if it failed. + */ + public synchronized boolean unfocus(Window window) { + if (window == null) { + Log.e(TAG, "Tried to unfocus a null window."); + return false; + } + return window.onFocus(false); + } + + /** + * Update the window corresponding to this id with the given params. + * + * @param id The id of the window. + * @param params The updated layout params to apply. + */ + public void updateViewLayout(int id, StandOutLayoutParams params) { + Window window = getWindow(id); + + if (window == null) { + Log.e(TAG, "Tried to updateViewLayout(" + id + ") a null window."); + return; + } + + if (window.visibility == Window.VISIBILITY_GONE) { + return; + } + + if (window.visibility == Window.VISIBILITY_TRANSITION) { + return; + } + + // alert callbacks and cancel if instructed + if (onUpdate(id, window, params)) { + Log.w(TAG, "Window " + id + " update cancelled by implementation."); + return; + } + + try { + window.setLayoutParams(params); + mWindowManager.updateViewLayout(window, params); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * LayoutParams specific to floating StandOut windows. + * + * @author Mark Wei + */ + public class StandOutLayoutParams extends WindowManager.LayoutParams { + /** + * Special value for x position that represents the left of the screen. + */ + public static final int LEFT = 0; + /** + * Special value for y position that represents the top of the screen. + */ + public static final int TOP = 0; + /** + * Special value for x position that represents the right of the screen. + */ + public static final int RIGHT = Integer.MAX_VALUE; + /** + * Special value for y position that represents the bottom of the + * screen. + */ + public static final int BOTTOM = Integer.MAX_VALUE; + /** + * Special value for x or y position that represents the center of the + * screen. + */ + public static final int CENTER = Integer.MIN_VALUE; + /** + * Special value for x or y position which requests that the system + * determine the position. + */ + public static final int AUTO_POSITION = Integer.MIN_VALUE + 1; + + /** + * The distance that distinguishes a tap from a drag. + */ + public int threshold; + + /** + * Optional constraints of the window. + */ + public int minWidth, minHeight, maxWidth, maxHeight; + + /** + * @param id The id of the window. + */ + public StandOutLayoutParams(int id) { + super(200, 200, TYPE_PHONE, + StandOutLayoutParams.FLAG_NOT_TOUCH_MODAL + | StandOutLayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSLUCENT); + + int windowFlags = getFlags(id); + + setFocusFlag(false); + + if (!Utils.isSet(windowFlags, + StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { + // windows may be moved beyond edges + flags |= FLAG_LAYOUT_NO_LIMITS; + } + + x = getX(id, width); + y = getY(id, height); + + gravity = Gravity.TOP | Gravity.LEFT; + + threshold = 10; + minWidth = minHeight = 0; + maxWidth = maxHeight = Integer.MAX_VALUE; + } + + /** + * @param id The id of the window. + * @param w The width of the window. + * @param h The height of the window. + */ + public StandOutLayoutParams(int id, int w, int h) { + this(id); + width = w; + height = h; + } + + /** + * @param id The id of the window. + * @param w The width of the window. + * @param h The height of the window. + * @param xpos The x position of the window. + * @param ypos The y position of the window. + */ + public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos) { + this(id, w, h); + + if (xpos != AUTO_POSITION) { + x = xpos; + } + if (ypos != AUTO_POSITION) { + y = ypos; + } + + Display display = mWindowManager.getDefaultDisplay(); + int width = display.getWidth(); + int height = display.getHeight(); + + if (x == RIGHT) { + x = width - w; + } else if (x == CENTER) { + x = (width - w) / 2; + } + + if (y == BOTTOM) { + y = height - h; + } else if (y == CENTER) { + y = (height - h) / 2; + } + } + + /** + * @param id The id of the window. + * @param w The width of the window. + * @param h The height of the window. + * @param xpos The x position of the window. + * @param ypos The y position of the window. + * @param minWidth The minimum width of the window. + * @param minHeight The mininum height of the window. + */ + public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos, + int minWidth, int minHeight) { + this(id, w, h, xpos, ypos); + + this.minWidth = minWidth; + this.minHeight = minHeight; + } + + /** + * @param id The id of the window. + * @param w The width of the window. + * @param h The height of the window. + * @param xpos The x position of the window. + * @param ypos The y position of the window. + * @param minWidth The minimum width of the window. + * @param minHeight The mininum height of the window. + * @param threshold The touch distance threshold that distinguishes a tap from + * a drag. + */ + public StandOutLayoutParams(int id, int w, int h, int xpos, int ypos, + int minWidth, int minHeight, int threshold) { + this(id, w, h, xpos, ypos, minWidth, minHeight); + + this.threshold = threshold; + } + + // helper to create cascading windows + private int getX(int id, int width) { + Display display = mWindowManager.getDefaultDisplay(); + int displayWidth = display.getWidth(); + + int types = sWindowCache.size(); + + int initialX = 100 * types; + int variableX = 100 * id; + int rawX = initialX + variableX; + + return rawX % (displayWidth - width); + } + + // helper to create cascading windows + private int getY(int id, int height) { + Display display = mWindowManager.getDefaultDisplay(); + int displayWidth = display.getWidth(); + int displayHeight = display.getHeight(); + + int types = sWindowCache.size(); + + int initialY = 100 * types; + int variableY = x + 200 * (100 * id) / (displayWidth - width); + + int rawY = initialY + variableY; + + return rawY % (displayHeight - height); + } + + public void setFocusFlag(boolean focused) { + if (focused) { + flags = flags ^ StandOutLayoutParams.FLAG_NOT_FOCUSABLE; + } else { + flags = flags | StandOutLayoutParams.FLAG_NOT_FOCUSABLE; + } + } + } + + protected class DropDownListItem { + public int icon; + public String description; + public Runnable action; + + public DropDownListItem(int icon, String description, Runnable action) { + super(); + this.icon = icon; + this.description = description; + this.action = action; + } + + @Override + public String toString() { + return description; + } + } +} diff --git a/integrationExample/app/src/main/java/wei/mark/standout/Utils.java b/integrationExample/app/src/main/java/wei/mark/standout/Utils.java new file mode 100644 index 0000000..85b4282 --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/Utils.java @@ -0,0 +1,7 @@ +package wei.mark.standout; + +public class Utils { + public static boolean isSet(int flags, int flag) { + return (flags & flag) == flag; + } +} diff --git a/integrationExample/app/src/main/java/wei/mark/standout/WindowCache.java b/integrationExample/app/src/main/java/wei/mark/standout/WindowCache.java new file mode 100644 index 0000000..c15f051 --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/WindowCache.java @@ -0,0 +1,133 @@ +package wei.mark.standout; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import wei.mark.standout.ui.Window; + +import android.util.SparseArray; + +public class WindowCache { + public Map, SparseArray> sWindows; + + public WindowCache() { + sWindows = new HashMap, SparseArray>(); + } + + /** + * Returns whether the window corresponding to the class and id exists in + * the {@link #sWindows} cache. + * + * @param id + * The id representing the window. + * @param cls + * Class corresponding to the window. + * @return True if the window corresponding to the class and id exists in + * the cache, or false if it does not exist. + */ + public boolean isCached(int id, Class cls) { + return getCache(id, cls) != null; + } + + /** + * Returns the window corresponding to the id from the {@link #sWindows} + * cache. + * + * @param id + * The id representing the window. + * @param cls + * The class of the implementation of the window. + * @return The window corresponding to the id if it exists in the cache, or + * null if it does not. + */ + public Window getCache(int id, Class cls) { + SparseArray l2 = sWindows.get(cls); + if (l2 == null) { + return null; + } + + return l2.get(id); + } + + /** + * Add the window corresponding to the id in the {@link #sWindows} cache. + * + * @param id + * The id representing the window. + * @param cls + * The class of the implementation of the window. + * @param window + * The window to be put in the cache. + */ + public void putCache(int id, Class cls, Window window) { + SparseArray l2 = sWindows.get(cls); + if (l2 == null) { + l2 = new SparseArray(); + sWindows.put(cls, l2); + } + + l2.put(id, window); + } + + /** + * Remove the window corresponding to the id from the {@link #sWindows} + * cache. + * + * @param id + * The id representing the window. + * @param cls + * The class of the implementation of the window. + */ + public void removeCache(int id, Class cls) { + SparseArray l2 = sWindows.get(cls); + if (l2 != null) { + l2.remove(id); + if (l2.size() == 0) { + sWindows.remove(cls); + } + } + } + + /** + * Returns the size of the {@link #sWindows} cache. + * + * @return True if the cache corresponding to this class is empty, false if + * it is not empty. + * @param cls + * The class of the implementation of the window. + */ + public int getCacheSize(Class cls) { + SparseArray l2 = sWindows.get(cls); + if (l2 == null) { + return 0; + } + + return l2.size(); + } + + /** + * Returns the ids in the {@link #sWindows} cache. + * + * @param cls + * The class of the implementation of the window. + * @return The ids representing the cached windows. + */ + public Set getCacheIds(Class cls) { + SparseArray l2 = sWindows.get(cls); + if (l2 == null) { + return new HashSet(); + } + + Set keys = new HashSet(); + for (int i = 0; i < l2.size(); i++) { + keys.add(l2.keyAt(i)); + } + return keys; + } + + public int size() { + return sWindows.size(); + } +} diff --git a/integrationExample/app/src/main/java/wei/mark/standout/constants/StandOutFlags.java b/integrationExample/app/src/main/java/wei/mark/standout/constants/StandOutFlags.java new file mode 100644 index 0000000..de454f5 --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/constants/StandOutFlags.java @@ -0,0 +1,218 @@ +package wei.mark.standout.constants; + +import wei.mark.standout.StandOutWindow; +import wei.mark.standout.ui.Window; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; + +/** + * Flags to be returned from {@link StandOutWindow#getFlags(int)}. + * + * @author Mark Wei + * + */ +public class StandOutFlags { + // This counter keeps track of which primary bit to set for each flag + private static int flag_bit = 0; + + /** + * Setting this flag indicates that the window wants the system provided + * window decorations (titlebar, hide/close buttons, resize handle, etc). + */ + public static final int FLAG_DECORATION_SYSTEM = 1 << flag_bit++; + + /** + * Setting this flag indicates that the window decorator should NOT provide + * a close button. + * + *

+ * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. + */ + public static final int FLAG_DECORATION_CLOSE_DISABLE = FLAG_DECORATION_SYSTEM + | 1 << flag_bit++; + + /** + * Setting this flag indicates that the window decorator should NOT provide + * a resize handle. + * + *

+ * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. + */ + public static final int FLAG_DECORATION_RESIZE_DISABLE = FLAG_DECORATION_SYSTEM + | 1 << flag_bit++; + + /** + * Setting this flag indicates that the window decorator should NOT provide + * a resize handle. + * + *

+ * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. + */ + public static final int FLAG_DECORATION_MAXIMIZE_DISABLE = FLAG_DECORATION_SYSTEM + | 1 << flag_bit++; + + /** + * Setting this flag indicates that the window decorator should NOT provide + * a resize handle. + * + *

+ * This flag also sets {@link #FLAG_DECORATION_SYSTEM}. + */ + public static final int FLAG_DECORATION_MOVE_DISABLE = FLAG_DECORATION_SYSTEM + | 1 << flag_bit++; + + /** + * Setting this flag indicates that the window can be moved by dragging the + * body. + * + *

+ * Note that if {@link #FLAG_DECORATION_SYSTEM} is set, the window can + * always be moved by dragging the titlebar regardless of this flag. + */ + public static final int FLAG_BODY_MOVE_ENABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that windows are able to be hidden, that + * {@link StandOutWindow#getHiddenIcon(int)}, + * {@link StandOutWindow#getHiddenTitle(int)}, and + * {@link StandOutWindow#getHiddenMessage(int)} are implemented, and that + * the system window decorator should provide a hide button if + * {@link #FLAG_DECORATION_SYSTEM} is set. + */ + public static final int FLAG_WINDOW_HIDE_ENABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the window should be brought to the + * front upon user interaction. + * + *

+ * Note that if you set this flag, there is a noticeable flashing of the + * window during {@link MotionEvent#ACTION_UP}. This the hack that allows + * the system to bring the window to the front. + */ + public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TOUCH = 1 << flag_bit++; + + /** + * Setting this flag indicates that the window should be brought to the + * front upon user tap. + * + *

+ * Note that if you set this flag, there is a noticeable flashing of the + * window during {@link MotionEvent#ACTION_UP}. This the hack that allows + * the system to bring the window to the front. + */ + public static final int FLAG_WINDOW_BRING_TO_FRONT_ON_TAP = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should keep the window's + * position within the edges of the screen. If this flag is not set, the + * window will be able to be dragged off of the screen. + * + *

+ * If this flag is set, the window's {@link Gravity} is recommended to be + * {@link Gravity#TOP} | {@link Gravity#LEFT}. If the gravity is anything + * other than TOP|LEFT, then even though the window will be displayed within + * the edges, it will behave as if the user can drag it off the screen. + * + */ + public static final int FLAG_WINDOW_EDGE_LIMITS_ENABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should keep the window's + * aspect ratio constant when resizing. + * + *

+ * The aspect ratio will only be enforced in + * {@link StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent)} + * . The aspect ratio will not be enforced if you set the width or height of + * the window's LayoutParams manually. + * + * @see StandOutWindow#onTouchHandleResize(int, Window, View, MotionEvent) + */ + public static final int FLAG_WINDOW_ASPECT_RATIO_ENABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should resize the window when + * it detects a pinch-to-zoom gesture. + * + * @see Window#onInterceptTouchEvent(MotionEvent) + */ + public static final int FLAG_WINDOW_PINCH_RESIZE_ENABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the window does not need focus. If this + * flag is set, the system will not take care of setting and unsetting the + * focus of windows based on user touch and key events. + * + *

+ * You will most likely need focus if your window contains any of the + * following: Button, ListView, EditText. + * + *

+ * The benefit of disabling focus is that your window will not consume any + * key events. Normally, focused windows will consume the Back and Menu + * keys. + * + * @see {@link StandOutWindow#focus(int)} + * @see {@link StandOutWindow#unfocus(int)} + * + */ + public static final int FLAG_WINDOW_FOCUSABLE_DISABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should not change the + * window's visual state when focus is changed. If this flag is set, the + * implementation can choose to change the visual state in + * {@link StandOutWindow#onFocusChange(int, Window, boolean)}. + * + * @see {@link Window#onFocus(boolean)} + * + */ + public static final int FLAG_WINDOW_FOCUS_INDICATOR_DISABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should disable all + * compatibility workarounds. The default behavior is to run + * {@link Window#fixCompatibility(View, int)} on the view returned by the + * implementation. + * + * @see {@link Window#fixCompatibility(View, int)} + */ + public static final int FLAG_FIX_COMPATIBILITY_ALL_DISABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should disable all additional + * functionality. The default behavior is to run + * {@link Window#addFunctionality(View, int)} on the view returned by the + * implementation. + * + * @see {@link StandOutWindow#addFunctionality(View, int)} + */ + public static final int FLAG_ADD_FUNCTIONALITY_ALL_DISABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should disable adding the + * resize handle additional functionality to a custom View R.id.corner. + * + *

+ * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able + * to resize the window with the default corner. + * + * @see {@link Window#addFunctionality(View, int)} + */ + public static final int FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE = 1 << flag_bit++; + + /** + * Setting this flag indicates that the system should disable adding the + * drop down menu additional functionality to a custom View + * R.id.window_icon. + * + *

+ * If {@link #FLAG_DECORATION_SYSTEM} is set, the user will always be able + * to show the drop down menu with the default window icon. + * + * @see {@link Window#addFunctionality(View, int)} + */ + public static final int FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE = 1 << flag_bit++; +} \ No newline at end of file diff --git a/integrationExample/app/src/main/java/wei/mark/standout/ui/TouchInfo.java b/integrationExample/app/src/main/java/wei/mark/standout/ui/TouchInfo.java new file mode 100644 index 0000000..5214596 --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/ui/TouchInfo.java @@ -0,0 +1,32 @@ +package wei.mark.standout.ui; + +import java.util.Locale; + +/** + * This class holds temporal touch and gesture information. Mainly used to hold + * temporary data for onTouchEvent(MotionEvent). + * + * @author Mark Wei + * + */ +public class TouchInfo { + /** + * The state of the window. + */ + public int firstX, firstY, lastX, lastY; + public double dist, scale, firstWidth, firstHeight; + public float ratio; + + /** + * Whether we're past the move threshold already. + */ + public boolean moving; + + @Override + public String toString() { + return String + .format(Locale.US, + "WindowTouchInfo { firstX=%d, firstY=%d,lastX=%d, lastY=%d, firstWidth=%d, firstHeight=%d }", + firstX, firstY, lastX, lastY, firstWidth, firstHeight); + } +} diff --git a/integrationExample/app/src/main/java/wei/mark/standout/ui/Window.java b/integrationExample/app/src/main/java/wei/mark/standout/ui/Window.java new file mode 100644 index 0000000..f9ae90d --- /dev/null +++ b/integrationExample/app/src/main/java/wei/mark/standout/ui/Window.java @@ -0,0 +1,845 @@ +package wei.mark.standout.ui; + +import android.content.Context; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.TextView; + +import net.alexandroid.floatingwindowexample.R; + +import java.util.LinkedList; +import java.util.Queue; + +import wei.mark.standout.StandOutWindow; +import wei.mark.standout.StandOutWindow.StandOutLayoutParams; +import wei.mark.standout.Utils; +import wei.mark.standout.constants.StandOutFlags; + +/** + * Special view that represents a floating window. + * + * @author Mark Wei + */ +public class Window extends FrameLayout { + public static final int VISIBILITY_GONE = 0; + public static final int VISIBILITY_VISIBLE = 1; + public static final int VISIBILITY_TRANSITION = 2; + + static final String TAG = "Window"; + + /** + * Class of the window, indicating which application the window belongs to. + */ + public Class cls; + /** + * Id of the window. + */ + public int id; + + /** + * Whether the window is shown, hidden/closed, or in transition. + */ + public int visibility; + + /** + * Whether the window is focused. + */ + public boolean focused; + + /** + * Original params from {@link StandOutWindow#getParams(int, Window)}. + */ + public StandOutLayoutParams originalParams; + /** + * Original flags from {@link StandOutWindow#getFlags(int)}. + */ + public int flags; + + /** + * Touch information of the window. + */ + public TouchInfo touchInfo; + + /** + * Data attached to the window. + */ + public Bundle data; + + /** + * Width and height of the screen. + */ + int displayWidth, displayHeight; + + /** + * Context of the window. + */ + private final StandOutWindow mContext; + private LayoutInflater mLayoutInflater; + + public Window(Context context) { + super(context); + mContext = null; + } + + public Window(final StandOutWindow context, final int id) { + super(context); + context.setTheme(context.getThemeStyle()); + + mContext = context; + mLayoutInflater = LayoutInflater.from(context); + + this.cls = context.getClass(); + this.id = id; + this.originalParams = context.getParams(id, this); + this.flags = context.getFlags(id); + this.touchInfo = new TouchInfo(); + touchInfo.ratio = (float) originalParams.width / originalParams.height; + this.data = new Bundle(); + DisplayMetrics metrics = mContext.getResources() + .getDisplayMetrics(); + displayWidth = metrics.widthPixels; + displayHeight = (int) (metrics.heightPixels - 25 * metrics.density); + + // create the window contents + View content; + FrameLayout body; + + if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) { + // requested system window decorations + content = getSystemDecorations(); + body = (FrameLayout) content.findViewById(R.id.body); + } else { + // did not request decorations. will provide own implementation + content = new FrameLayout(context); + content.setId(R.id.content); + body = (FrameLayout) content; + } + + addView(content); + + body.setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // pass all touch events to the implementation + boolean consumed = false; + + // handle move and bring to front + consumed = context.onTouchHandleMove(id, Window.this, v, event) + || consumed; + + // alert implementation + consumed = context.onTouchBody(id, Window.this, v, event) + || consumed; + + return consumed; + } + }); + + // attach the view corresponding to the id from the + // implementation + context.createAndAttachView(id, body); + + // make sure the implementation attached the view + if (body.getChildCount() == 0) { + throw new RuntimeException( + "You must attach your view to the given frame in createAndAttachView()"); + } + + // implement StandOut specific workarounds + if (!Utils.isSet(flags, + StandOutFlags.FLAG_FIX_COMPATIBILITY_ALL_DISABLE)) { + fixCompatibility(body); + } + // implement StandOut specific additional functionality + if (!Utils.isSet(flags, + StandOutFlags.FLAG_ADD_FUNCTIONALITY_ALL_DISABLE)) { + addFunctionality(body); + } + + // attach the existing tag from the frame to the window + setTag(body.getTag()); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + StandOutLayoutParams params = getLayoutParams(); + + // focus window + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (mContext.getFocusedWindow() != this) { + mContext.focus(id); + } + } + + // multitouch + if (event.getPointerCount() >= 2 + && Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE) + && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { + touchInfo.scale = 1; + touchInfo.dist = -1; + touchInfo.firstWidth = params.width; + touchInfo.firstHeight = params.height; + return true; + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // handle touching outside + switch (event.getAction()) { + case MotionEvent.ACTION_OUTSIDE: + // unfocus window + if (mContext.getFocusedWindow() == this) { + mContext.unfocus(this); + } + + // notify implementation that ACTION_OUTSIDE occurred + mContext.onTouchBody(id, this, this, event); + break; + } + + // handle multitouch + if (event.getPointerCount() >= 2 + && Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE)) { + // 2 fingers or more + + float x0 = event.getX(0); + float y0 = event.getY(0); + float x1 = event.getX(1); + float y1 = event.getY(1); + + double dist = Math + .sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: + if (touchInfo.dist == -1) { + touchInfo.dist = dist; + } + touchInfo.scale *= dist / touchInfo.dist; + touchInfo.dist = dist; + + // scale the window with anchor point set to middle + edit().setAnchorPoint(.5f, .5f) + .setSize( + (int) (touchInfo.firstWidth * touchInfo.scale), + (int) (touchInfo.firstHeight * touchInfo.scale)) + .commit(); + break; + } + mContext.onResize(id, this, this, event); + } + + return true; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mContext.onKeyEvent(id, this, event)) { + Log.d(TAG, "Window " + id + " key event " + event + + " cancelled by implementation."); + return false; + } + + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_BACK: + mContext.unfocus(this); + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + /** + * Request or remove the focus from this window. + * + * @param focus Whether we want to gain or lose focus. + * @return True if focus changed successfully, false if it failed. + */ + public boolean onFocus(boolean focus) { + if (!Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) { + // window is focusable + + if (focus == focused) { + // window already focused/unfocused + return false; + } + + focused = focus; + + // alert callbacks and cancel if instructed + if (mContext.onFocusChange(id, this, focus)) { + Log.d(TAG, "Window " + id + " focus change " + + (focus ? "(true)" : "(false)") + + " cancelled by implementation."); + focused = !focus; + return false; + } + + if (!Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_FOCUS_INDICATOR_DISABLE)) { + // change visual state + View content = findViewById(R.id.content); + if (focus) { + // gaining focus + content.setBackgroundResource(R.drawable.border_focused); + } else { + // losing focus + if (Utils + .isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) { + // system decorations + content.setBackgroundResource(R.drawable.border); + } else { + // no decorations + content.setBackgroundResource(0); + } + } + } + + // set window manager params + StandOutLayoutParams params = getLayoutParams(); + params.setFocusFlag(focus); + mContext.updateViewLayout(id, params); + + if (focus) { + mContext.setFocusedWindow(this); + } else { + if (mContext.getFocusedWindow() == this) { + mContext.setFocusedWindow(null); + } + } + + return true; + } + return false; + } + + @Override + public void setLayoutParams(ViewGroup.LayoutParams params) { + if (params instanceof StandOutLayoutParams) { + super.setLayoutParams(params); + } else { + throw new IllegalArgumentException( + "Window" + + id + + ": LayoutParams must be an instance of StandOutLayoutParams."); + } + } + + /** + * Convenience method to start editting the size and position of this + * window. Make sure you call {@link Editor#commit()} when you are done to + * update the window. + * + * @return The Editor associated with this window. + */ + public Editor edit() { + return new Editor(); + } + + @Override + public StandOutLayoutParams getLayoutParams() { + StandOutLayoutParams params = (StandOutLayoutParams) super + .getLayoutParams(); + if (params == null) { + params = originalParams; + } + return params; + } + + /** + * Returns the system window decorations if the implementation sets + * {@link #FLAG_DECORATION_SYSTEM}. + *

+ *

+ * The system window decorations support hiding, closing, moving, and + * resizing. + * + * @return The frame view containing the system window decorations. + */ + private View getSystemDecorations() { + final View decorations = mLayoutInflater.inflate( + R.layout.system_window_decorators, null); + + // icon + final ImageView icon = (ImageView) decorations + .findViewById(R.id.window_icon); + icon.setImageResource(mContext.getAppIcon()); + icon.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PopupWindow dropDown = mContext.getDropDown(id); + if (dropDown != null) { + dropDown.showAsDropDown(icon); + } + } + }); + + // title + TextView title = (TextView) decorations.findViewById(R.id.title); + title.setText(mContext.getTitle(id)); + + // hide + View hide = decorations.findViewById(R.id.hide); + hide.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + mContext.hide(id); + } + }); + hide.setVisibility(View.GONE); + + // maximize + View maximize = decorations.findViewById(R.id.maximize); + maximize.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + StandOutLayoutParams params = getLayoutParams(); + boolean isMaximized = data + .getBoolean(WindowDataKeys.IS_MAXIMIZED); + if (isMaximized && params.width == displayWidth + && params.height == displayHeight && params.x == 0 + && params.y == 0) { + data.putBoolean(WindowDataKeys.IS_MAXIMIZED, false); + int oldWidth = data.getInt( + WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, -1); + int oldHeight = data.getInt( + WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, -1); + int oldX = data + .getInt(WindowDataKeys.X_BEFORE_MAXIMIZE, -1); + int oldY = data + .getInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, -1); + edit().setSize(oldWidth, oldHeight).setPosition(oldX, oldY) + .commit(); + } else { + data.putBoolean(WindowDataKeys.IS_MAXIMIZED, true); + data.putInt(WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, + params.width); + data.putInt(WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, + params.height); + data.putInt(WindowDataKeys.X_BEFORE_MAXIMIZE, params.x); + data.putInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, params.y); + edit().setSize(1f, 1f).setPosition(0, 0).commit(); + } + } + }); + + // close + View close = decorations.findViewById(R.id.close); + close.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + mContext.close(id); + } + }); + + // move + View titlebar = decorations.findViewById(R.id.titlebar); + titlebar.setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // handle dragging to move + boolean consumed = mContext.onTouchHandleMove(id, Window.this, + v, event); + return consumed; + } + }); + + // resize + View corner = decorations.findViewById(R.id.corner); + corner.setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // handle dragging to move + boolean consumed = mContext.onTouchHandleResize(id, + Window.this, v, event); + + return consumed; + } + }); + + // set window appearance and behavior based on flags + if (Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) { + hide.setVisibility(View.VISIBLE); + } + if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MAXIMIZE_DISABLE)) { + maximize.setVisibility(View.GONE); + } + if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_CLOSE_DISABLE)) { + close.setVisibility(View.GONE); + } + if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MOVE_DISABLE)) { + titlebar.setOnTouchListener(null); + } + if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_RESIZE_DISABLE)) { + corner.setVisibility(View.GONE); + } + + return decorations; + } + + /** + * Implement StandOut specific additional functionalities. + *

+ *

+ * Currently, this method does the following: + *

+ *

+ * Attach resize handles: For every View found to have id R.id.corner, + * attach an OnTouchListener that implements resizing the window. + * + * @param root The view hierarchy that is part of the window. + */ + void addFunctionality(View root) { + // corner for resize + if (!Utils.isSet(flags, + StandOutFlags.FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE)) { + View corner = root.findViewById(R.id.corner); + if (corner != null) { + corner.setOnTouchListener(new OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // handle dragging to move + boolean consumed = mContext.onTouchHandleResize(id, + Window.this, v, event); + + return consumed; + } + }); + } + } + + // window_icon for drop down + if (!Utils.isSet(flags, + StandOutFlags.FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE)) { + final View icon = root.findViewById(R.id.window_icon); + if (icon != null) { + icon.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PopupWindow dropDown = mContext.getDropDown(id); + if (dropDown != null) { + dropDown.showAsDropDown(icon); + } + } + }); + } + } + } + + /** + * Iterate through each View in the view hiearchy and implement StandOut + * specific compatibility workarounds. + *

+ *

+ * Currently, this method does the following: + *

+ *

+ * Nothing yet. + * + * @param root The root view hierarchy to iterate through and check. + */ + void fixCompatibility(View root) { + Queue queue = new LinkedList(); + queue.add(root); + + View view = null; + while ((view = queue.poll()) != null) { + // do nothing yet + + // iterate through children + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + queue.add(group.getChildAt(i)); + } + } + } + } + + /** + * Convenient way to resize or reposition a Window. The Editor allows you to + * easily resize and reposition the window around anchor points. + * + * @author Mark Wei + */ + public class Editor { + /** + * Special value for width, height, x, or y positions that represents + * that the value should not be changed. + */ + public static final int UNCHANGED = Integer.MIN_VALUE; + + /** + * Layout params of the window associated with this Editor. + */ + StandOutLayoutParams mParams; + + /** + * The position of the anchor point as a percentage of the window's + * width/height. The anchor point is only used by the {@link Editor}. + *

+ *

+ * The anchor point effects the following methods: + *

+ *

+ * {@link #setSize(float, float)}, {@link #setSize(int, int)}, + * {@link #setPosition(int, int)}, {@link #setPosition(int, int)}. + *

+ * The window will move, expand, or shrink around the anchor point. + *

+ *

+ * Values must be between 0 and 1, inclusive. 0 means the left/top, 0.5 + * is the center, 1 is the right/bottom. + */ + float anchorX, anchorY; + + public Editor() { + mParams = getLayoutParams(); + anchorX = anchorY = 0; + } + + public Editor setAnchorPoint(float x, float y) { + if (x < 0 || x > 1 || y < 0 || y > 1) { + throw new IllegalArgumentException( + "Anchor point must be between 0 and 1, inclusive."); + } + + anchorX = x; + anchorY = y; + + return this; + } + + /** + * Set the size of this window as percentages of max screen size. The + * window will expand and shrink around the top-left corner, unless + * you've set a different anchor point with + * {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param percentWidth + * @param percentHeight + * @return The same Editor, useful for method chaining. + */ + public Editor setSize(float percentWidth, float percentHeight) { + return setSize((int) (displayWidth * percentWidth), + (int) (displayHeight * percentHeight)); + } + + /** + * Set the size of this window in absolute pixels. The window will + * expand and shrink around the top-left corner, unless you've set a + * different anchor point with {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param width + * @param height + * @return The same Editor, useful for method chaining. + */ + public Editor setSize(int width, int height) { + return setSize(width, height, false); + } + + /** + * Set the size of this window in absolute pixels. The window will + * expand and shrink around the top-left corner, unless you've set a + * different anchor point with {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param width + * @param height + * @param skip Don't call {@link #setPosition(int, int)} to avoid stack + * overflow. + * @return The same Editor, useful for method chaining. + */ + private Editor setSize(int width, int height, boolean skip) { + if (mParams != null) { + if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { + throw new IllegalStateException( + "Anchor point must be between 0 and 1, inclusive."); + } + + int lastWidth = mParams.width; + int lastHeight = mParams.height; + + if (width != UNCHANGED) { + mParams.width = width; + } + if (height != UNCHANGED) { + mParams.height = height; + } + + // set max width/height + int maxWidth = mParams.maxWidth; + int maxHeight = mParams.maxHeight; + + if (Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { + maxWidth = (int) Math.min(maxWidth, displayWidth); + maxHeight = (int) Math.min(maxHeight, displayHeight); + } + + // keep window between min and max + mParams.width = Math.min( + Math.max(mParams.width, mParams.minWidth), maxWidth); + mParams.height = Math.min( + Math.max(mParams.height, mParams.minHeight), maxHeight); + + // keep window in aspect ratio + if (Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_ASPECT_RATIO_ENABLE)) { + int ratioWidth = (int) (mParams.height * touchInfo.ratio); + int ratioHeight = (int) (mParams.width / touchInfo.ratio); + if (ratioHeight >= mParams.minHeight + && ratioHeight <= mParams.maxHeight) { + // width good adjust height + mParams.height = ratioHeight; + } else { + // height good adjust width + mParams.width = ratioWidth; + } + } + + if (!skip) { + // set position based on anchor point + setPosition((int) (mParams.x + lastWidth * anchorX), + (int) (mParams.y + lastHeight * anchorY)); + } + } + + return this; + } + + /** + * Set the position of this window as percentages of max screen size. + * The window's top-left corner will be positioned at the given x and y, + * unless you've set a different anchor point with + * {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param percentWidth + * @param percentHeight + * @return The same Editor, useful for method chaining. + */ + public Editor setPosition(float percentWidth, float percentHeight) { + return setPosition((int) (displayWidth * percentWidth), + (int) (displayHeight * percentHeight)); + } + + /** + * Set the position of this window in absolute pixels. The window's + * top-left corner will be positioned at the given x and y, unless + * you've set a different anchor point with + * {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param x + * @param y + * @return The same Editor, useful for method chaining. + */ + public Editor setPosition(int x, int y) { + return setPosition(x, y, false); + } + + /** + * Set the position of this window in absolute pixels. The window's + * top-left corner will be positioned at the given x and y, unless + * you've set a different anchor point with + * {@link #setAnchorPoint(float, float)}. + *

+ * Changes will not applied until you {@link #commit()}. + * + * @param x + * @param y + * @param skip Don't call {@link #setPosition(int, int)} and + * {@link #setSize(int, int)} to avoid stack overflow. + * @return The same Editor, useful for method chaining. + */ + private Editor setPosition(int x, int y, boolean skip) { + if (mParams != null) { + if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) { + throw new IllegalStateException( + "Anchor point must be between 0 and 1, inclusive."); + } + + // sets the x and y correctly according to anchorX and + // anchorY + if (x != UNCHANGED) { + mParams.x = (int) (x - mParams.width * anchorX); + } + if (y != UNCHANGED) { + mParams.y = (int) (y - mParams.height * anchorY); + } + + if (Utils.isSet(flags, + StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) { + // if gravity is not TOP|LEFT throw exception + if (mParams.gravity != (Gravity.TOP | Gravity.LEFT)) { + throw new IllegalStateException( + "The window " + + id + + " gravity must be TOP|LEFT if FLAG_WINDOW_EDGE_LIMITS_ENABLE or FLAG_WINDOW_EDGE_TILE_ENABLE is set."); + } + + // keep window inside edges + mParams.x = Math.min(Math.max(mParams.x, 0), displayWidth + - mParams.width); + mParams.y = Math.min(Math.max(mParams.y, 0), displayHeight + - mParams.height); + } + } + + return this; + } + + /** + * Commit the changes to this window. Updates the layout. This Editor + * cannot be used after you commit. + */ + public void commit() { + if (mParams != null) { + mContext.updateViewLayout(id, mParams); + mParams = null; + } + } + } + + public static class WindowDataKeys { + public static final String IS_MAXIMIZED = "isMaximized"; + public static final String WIDTH_BEFORE_MAXIMIZE = "widthBeforeMaximize"; + public static final String HEIGHT_BEFORE_MAXIMIZE = "heightBeforeMaximize"; + public static final String X_BEFORE_MAXIMIZE = "xBeforeMaximize"; + public static final String Y_BEFORE_MAXIMIZE = "yBeforeMaximize"; + } +} \ No newline at end of file diff --git a/integrationExample/app/src/main/res/drawable-hdpi/border.9.png b/integrationExample/app/src/main/res/drawable-hdpi/border.9.png new file mode 100644 index 0000000..02c53a4 Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/border.9.png differ diff --git a/integrationExample/app/src/main/res/drawable-hdpi/border_focused.9.png b/integrationExample/app/src/main/res/drawable-hdpi/border_focused.9.png new file mode 100644 index 0000000..76bbe5a Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/border_focused.9.png differ diff --git a/integrationExample/app/src/main/res/drawable-hdpi/close.png b/integrationExample/app/src/main/res/drawable-hdpi/close.png new file mode 100644 index 0000000..fb0b119 Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/close.png differ diff --git a/integrationExample/app/src/main/res/drawable-hdpi/corner.png b/integrationExample/app/src/main/res/drawable-hdpi/corner.png new file mode 100644 index 0000000..b143410 Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/corner.png differ diff --git a/integrationExample/app/src/main/res/drawable-hdpi/hide.png b/integrationExample/app/src/main/res/drawable-hdpi/hide.png new file mode 100644 index 0000000..f44d282 Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/hide.png differ diff --git a/integrationExample/app/src/main/res/drawable-hdpi/maximize.png b/integrationExample/app/src/main/res/drawable-hdpi/maximize.png new file mode 100644 index 0000000..b5ed83f Binary files /dev/null and b/integrationExample/app/src/main/res/drawable-hdpi/maximize.png differ diff --git a/integrationExample/app/src/main/res/layout/activity_main.xml b/integrationExample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6516002 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/integrationExample/app/src/main/res/layout/body.xml b/integrationExample/app/src/main/res/layout/body.xml new file mode 100644 index 0000000..a4bb933 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/body.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/src/main/res/layout/content_main.xml b/integrationExample/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..1fb534d --- /dev/null +++ b/integrationExample/app/src/main/res/layout/content_main.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/integrationExample/app/src/main/res/layout/drop_down_list_item.xml b/integrationExample/app/src/main/res/layout/drop_down_list_item.xml new file mode 100644 index 0000000..6a5a5d4 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/drop_down_list_item.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/src/main/res/layout/simple.xml b/integrationExample/app/src/main/res/layout/simple.xml new file mode 100644 index 0000000..1cbf897 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/simple.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/src/main/res/layout/system_window_decorators.xml b/integrationExample/app/src/main/res/layout/system_window_decorators.xml new file mode 100644 index 0000000..9d523f8 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/system_window_decorators.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrationExample/app/src/main/res/layout/widgets.xml b/integrationExample/app/src/main/res/layout/widgets.xml new file mode 100644 index 0000000..0336c07 --- /dev/null +++ b/integrationExample/app/src/main/res/layout/widgets.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + +