diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 80feb86..5e8d8e8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ - + - + + android:theme="@android:style/Theme.Holo.Light"> diff --git a/README.md b/README.md index 0941a94..aa4275d 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,5 @@ The next generation NetworkLocationProvider, based on plugins `sample` contains a sample plugin. To be build with Android Build System using `make UnifiedNlp` + +Some components: Copyright (C) 2013 The Android Open Source Project diff --git a/api/src/org/microg/nlp/api/NlpApiConstants.java b/api/src/org/microg/nlp/api/NlpApiConstants.java index 1db32b5..bdfb100 100644 --- a/api/src/org/microg/nlp/api/NlpApiConstants.java +++ b/api/src/org/microg/nlp/api/NlpApiConstants.java @@ -2,4 +2,5 @@ package org.microg.nlp.api; public class NlpApiConstants { public static final String ACTION_LOCATION_BACKEND = "org.microg.nlp.LOCATION_BACKEND"; + public static final String METADATA_BACKEND_SETTINGS_ACTIVITY = "org.microg.nlp.BACKEND_SETTINGS_ACTIVITY"; } diff --git a/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png b/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png new file mode 100644 index 0000000..bb6aef1 Binary files /dev/null and b/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png differ diff --git a/res/drawable-xhdpi/ic_action_add.png b/res/drawable-xhdpi/ic_action_add.png new file mode 100644 index 0000000..b74cb31 Binary files /dev/null and b/res/drawable-xhdpi/ic_action_add.png differ diff --git a/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png b/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png new file mode 100644 index 0000000..930ca8d Binary files /dev/null and b/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png differ diff --git a/res/layout/backend_list_entry.xml b/res/layout/backend_list_entry.xml new file mode 100644 index 0000000..57d4b9e --- /dev/null +++ b/res/layout/backend_list_entry.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/pluginselection.xml b/res/layout/pluginselection.xml new file mode 100644 index 0000000..a70a64f --- /dev/null +++ b/res/layout/pluginselection.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index aa4401b..aa39d58 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,4 +1,5 @@ NLP Controller + Add plugin \ No newline at end of file diff --git a/sample/AndroidManifest.xml b/sample/AndroidManifest.xml index 6b34e2f..ef3d4c5 100644 --- a/sample/AndroidManifest.xml +++ b/sample/AndroidManifest.xml @@ -1,6 +1,6 @@ - + + android:exported="true" + android:label="NLPV2-Sample"> + + + + + + + + diff --git a/sample/src/org/microg/nlp/api/sample/SecondSampleService.java b/sample/src/org/microg/nlp/api/sample/SecondSampleService.java new file mode 100644 index 0000000..92cc4b6 --- /dev/null +++ b/sample/src/org/microg/nlp/api/sample/SecondSampleService.java @@ -0,0 +1,15 @@ +package org.microg.nlp.api.sample; + +import android.location.Location; +import org.microg.nlp.api.LocationBackendService; + +public class SecondSampleService extends LocationBackendService { + @Override + protected Location update() { + Location location = new Location("second-sample"); + location.setLatitude(13); + location.setLongitude(13); + location.setAccuracy(100); + return location; + } +} diff --git a/sample/src/org/microg/nlp/api/sample/SecondSettings.java b/sample/src/org/microg/nlp/api/sample/SecondSettings.java new file mode 100644 index 0000000..b255592 --- /dev/null +++ b/sample/src/org/microg/nlp/api/sample/SecondSettings.java @@ -0,0 +1,10 @@ +package org.microg.nlp.api.sample; + +import android.app.Activity; +import android.os.Bundle; + +public class SecondSettings extends Activity { + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} \ No newline at end of file diff --git a/src/org/microg/nlp/geocode/GeocodeService.java b/src/org/microg/nlp/geocode/GeocodeService.java index 4a4853a..85e6cb2 100644 --- a/src/org/microg/nlp/geocode/GeocodeService.java +++ b/src/org/microg/nlp/geocode/GeocodeService.java @@ -1,8 +1,5 @@ package org.microg.nlp.geocode; -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; import org.microg.nlp.ProviderService; public abstract class GeocodeService extends ProviderService { diff --git a/src/org/microg/nlp/ui/DynamicListView.java b/src/org/microg/nlp/ui/DynamicListView.java new file mode 100644 index 0000000..161942f --- /dev/null +++ b/src/org/microg/nlp/ui/DynamicListView.java @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.nlp.ui; + +import android.animation.*; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.graphics.*; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.*; + +import java.util.HashMap; +import java.util.List; + +/** + * The dynamic listview is an extension of listview that supports cell dragging + * and swapping. + *

+ * This layout is in charge of positioning the hover cell in the correct location + * on the screen in response to user touch events. It uses the position of the + * hover cell to determine when two cells should be swapped. If two cells should + * be swapped, all the corresponding data set and layout changes are handled here. + *

+ * If no cell is selected, all the touch events are passed down to the listview + * and behave normally. If one of the items in the listview experiences a + * long press event, the contents of its current visible state are captured as + * a bitmap and its visibility is set to INVISIBLE. A hover cell is then created and + * added to this layout as an overlaying BitmapDrawable above the listview. Once the + * hover cell is translated some distance to signify an item swap, a data set change + * accompanied by animation takes place. When the user releases the hover cell, + * it animates into its corresponding position in the listview. + *

+ * When the hover cell is either above or below the bounds of the listview, this + * listview also scrolls on its own so as to reveal additional content. + */ +public class DynamicListView extends ListView { + + /** + * This TypeEvaluator is used to animate the BitmapDrawable back to its + * final location when the user lifts his finger by modifying the + * BitmapDrawable's bounds. + */ + private final static TypeEvaluator sBoundEvaluator = new TypeEvaluator() { + public Rect evaluate(float fraction, Rect startValue, Rect endValue) { + return new Rect(interpolate(startValue.left, endValue.left, fraction), + interpolate(startValue.top, endValue.top, fraction), + interpolate(startValue.right, endValue.right, fraction), + interpolate(startValue.bottom, endValue.bottom, fraction)); + } + + public int interpolate(int start, int end, float fraction) { + return (int) (start + fraction * (end - start)); + } + }; + private final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 15; + private final int MOVE_DURATION = 150; + private final int LINE_THICKNESS = 15; + private final int INVALID_ID = -1; + private long mBelowItemId = INVALID_ID; + private long mMobileItemId = INVALID_ID; + private long mAboveItemId = INVALID_ID; + private final int INVALID_POINTER_ID = -1; + private int mActivePointerId = INVALID_POINTER_ID; + public List mList; + private int mLastEventY = -1; + private int mDownY = -1; + private int mDownX = -1; + private int mTotalOffset = 0; + private boolean mCellIsMobile = false; + private boolean mIsMobileScrolling = false; + private int mSmoothScrollAmountAtEdge = 0; + private BitmapDrawable mHoverCell; + /** + * Listens for long clicks on any items in the listview. When a cell has + * been selected, the hover cell is created and set up. + */ + private AdapterView.OnItemLongClickListener mOnItemLongClickListener = + new AdapterView.OnItemLongClickListener() { + public boolean onItemLongClick(AdapterView arg0, View arg1, int pos, long id) { + mTotalOffset = 0; + + int position = pointToPosition(mDownX, mDownY); + int itemNum = position - getFirstVisiblePosition(); + + View selectedView = getChildAt(itemNum); + mMobileItemId = getAdapter().getItemId(position); + mHoverCell = getAndAddHoverView(selectedView); + selectedView.setVisibility(INVISIBLE); + + mCellIsMobile = true; + + updateNeighborViewsForID(mMobileItemId); + + return true; + } + }; + private Rect mHoverCellCurrentBounds; + private Rect mHoverCellOriginalBounds; + private boolean mIsWaitingForScrollFinish = false; + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + public DynamicListView(Context context) { + super(context); + init(context); + } + + public DynamicListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public DynamicListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public void init(Context context) { + setOnItemLongClickListener(mOnItemLongClickListener); + setOnScrollListener(mScrollListener); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mSmoothScrollAmountAtEdge = (int) (SMOOTH_SCROLL_AMOUNT_AT_EDGE / metrics.density); + } + + /** + * Creates the hover cell with the appropriate bitmap and of appropriate + * size. The hover cell's BitmapDrawable is drawn on top of the bitmap every + * single time an invalidate call is made. + */ + private BitmapDrawable getAndAddHoverView(View v) { + + int w = v.getWidth(); + int h = v.getHeight(); + int top = v.getTop(); + int left = v.getLeft(); + + Bitmap b = getBitmapWithBorder(v); + + BitmapDrawable drawable = new BitmapDrawable(getResources(), b); + + mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h); + mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds); + + drawable.setBounds(mHoverCellCurrentBounds); + + return drawable; + } + + /** + * Draws a black border over the screenshot of the view passed in. + */ + private Bitmap getBitmapWithBorder(View v) { + Bitmap bitmap = getBitmapFromView(v); + Canvas can = new Canvas(bitmap); + + Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(LINE_THICKNESS); + paint.setColor(Color.BLACK); + + can.drawBitmap(bitmap, 0, 0, null); + can.drawRect(rect, paint); + + return bitmap; + } + + /** + * Returns a bitmap showing a screenshot of the view passed in. + */ + private Bitmap getBitmapFromView(View v) { + Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + v.draw(canvas); + return bitmap; + } + + /** + * Stores a reference to the views above and below the item currently + * corresponding to the hover cell. It is important to note that if this + * item is either at the top or bottom of the list, mAboveItemId or mBelowItemId + * may be invalid. + */ + private void updateNeighborViewsForID(long itemID) { + int position = getPositionForID(itemID); + StableArrayAdapter adapter = ((StableArrayAdapter) getAdapter()); + mAboveItemId = adapter.getItemId(position - 1); + mBelowItemId = adapter.getItemId(position + 1); + } + + /** + * Retrieves the view in the list corresponding to itemID + */ + public View getViewForID(long itemID) { + int firstVisiblePosition = getFirstVisiblePosition(); + StableArrayAdapter adapter = ((StableArrayAdapter) getAdapter()); + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + int position = firstVisiblePosition + i; + long id = adapter.getItemId(position); + if (id == itemID) { + return v; + } + } + return null; + } + + /** + * Retrieves the position in the list corresponding to itemID + */ + public int getPositionForID(long itemID) { + View v = getViewForID(itemID); + if (v == null) { + return -1; + } else { + return getPositionForView(v); + } + } + + /** + * dispatchDraw gets invoked when all the child views are about to be drawn. + * By overriding this method, the hover cell (BitmapDrawable) can be drawn + * over the listview's items whenever the listview is redrawn. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mHoverCell != null) { + mHoverCell.draw(canvas); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mDownX = (int) event.getX(); + mDownY = (int) event.getY(); + mActivePointerId = event.getPointerId(0); + break; + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER_ID) { + break; + } + + int pointerIndex = event.findPointerIndex(mActivePointerId); + + mLastEventY = (int) event.getY(pointerIndex); + int deltaY = mLastEventY - mDownY; + + if (mCellIsMobile) { + mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, + mHoverCellOriginalBounds.top + deltaY + mTotalOffset); + mHoverCell.setBounds(mHoverCellCurrentBounds); + invalidate(); + + handleCellSwitch(); + + mIsMobileScrolling = false; + handleMobileCellScroll(); + + return false; + } + break; + case MotionEvent.ACTION_UP: + touchEventsEnded(); + break; + case MotionEvent.ACTION_CANCEL: + touchEventsCancelled(); + break; + case MotionEvent.ACTION_POINTER_UP: + /* If a multitouch event took place and the original touch dictating + * the movement of the hover cell has ended, then the dragging event + * ends and the hover cell is animated to its corresponding position + * in the listview. */ + pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + touchEventsEnded(); + } + break; + default: + break; + } + + return super.onTouchEvent(event); + } + + /** + * This method determines whether the hover cell has been shifted far enough + * to invoke a cell swap. If so, then the respective cell swap candidate is + * determined and the data set is changed. Upon posting a notification of the + * data set change, a layout is invoked to place the cells in the right place. + * Using a ViewTreeObserver and a corresponding OnPreDrawListener, we can + * offset the cell being swapped to where it previously was and then animate it to + * its new position. + */ + private void handleCellSwitch() { + final int deltaY = mLastEventY - mDownY; + int deltaYTotal = mHoverCellOriginalBounds.top + mTotalOffset + deltaY; + + View belowView = getViewForID(mBelowItemId); + View mobileView = getViewForID(mMobileItemId); + View aboveView = getViewForID(mAboveItemId); + + boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop()); + boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop()); + + if (isBelow || isAbove) { + + final long switchItemID = isBelow ? mBelowItemId : mAboveItemId; + View switchView = isBelow ? belowView : aboveView; + final int originalItem = getPositionForView(mobileView); + + if (switchView == null) { + updateNeighborViewsForID(mMobileItemId); + return; + } + + swapElements(mList, originalItem, getPositionForView(switchView)); + + ((BaseAdapter) getAdapter()).notifyDataSetChanged(); + + mDownY = mLastEventY; + + final int switchViewStartTop = switchView.getTop(); + + mobileView.setVisibility(View.VISIBLE); + switchView.setVisibility(View.INVISIBLE); + + updateNeighborViewsForID(mMobileItemId); + + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + observer.removeOnPreDrawListener(this); + + View switchView = getViewForID(switchItemID); + + mTotalOffset += deltaY; + + int switchViewNewTop = switchView.getTop(); + int delta = switchViewStartTop - switchViewNewTop; + + switchView.setTranslationY(delta); + + ObjectAnimator animator = ObjectAnimator.ofFloat(switchView, + View.TRANSLATION_Y, 0); + animator.setDuration(MOVE_DURATION); + animator.start(); + + return true; + } + }); + } + } + + private void swapElements(List arrayList, int indexOne, int indexTwo) { + Object temp = arrayList.get(indexOne); + arrayList.set(indexOne, arrayList.get(indexTwo)); + arrayList.set(indexTwo, temp); + } + + /** + * Resets all the appropriate fields to a default state while also animating + * the hover cell back to its correct location. + */ + private void touchEventsEnded() { + final View mobileView = getViewForID(mMobileItemId); + if (mCellIsMobile || mIsWaitingForScrollFinish) { + mCellIsMobile = false; + mIsWaitingForScrollFinish = false; + mIsMobileScrolling = false; + mActivePointerId = INVALID_POINTER_ID; + + // If the autoscroller has not completed scrolling, we need to wait for it to + // finish in order to determine the final location of where the hover cell + // should be animated to. + if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) { + mIsWaitingForScrollFinish = true; + return; + } + + mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mobileView.getTop()); + + ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds", + sBoundEvaluator, mHoverCellCurrentBounds); + hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + invalidate(); + } + }); + hoverViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setEnabled(false); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAboveItemId = INVALID_ID; + mMobileItemId = INVALID_ID; + mBelowItemId = INVALID_ID; + mobileView.setVisibility(VISIBLE); + mHoverCell = null; + setEnabled(true); + invalidate(); + } + }); + hoverViewAnimator.start(); + } else { + touchEventsCancelled(); + } + } + + /** + * Resets all the appropriate fields to a default state. + */ + private void touchEventsCancelled() { + View mobileView = getViewForID(mMobileItemId); + if (mCellIsMobile) { + mAboveItemId = INVALID_ID; + mMobileItemId = INVALID_ID; + mBelowItemId = INVALID_ID; + mobileView.setVisibility(VISIBLE); + mHoverCell = null; + invalidate(); + } + mCellIsMobile = false; + mIsMobileScrolling = false; + mActivePointerId = INVALID_POINTER_ID; + } + + /** + * Determines whether this listview is in a scrolling state invoked + * by the fact that the hover cell is out of the bounds of the listview; + */ + private void handleMobileCellScroll() { + mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds); + } + + /** + * This method is in charge of determining if the hover cell is above + * or below the bounds of the listview. If so, the listview does an appropriate + * upward or downward smooth scroll so as to reveal new items. + */ + public boolean handleMobileCellScroll(Rect r) { + int offset = computeVerticalScrollOffset(); + int height = getHeight(); + int extent = computeVerticalScrollExtent(); + int range = computeVerticalScrollRange(); + int hoverViewTop = r.top; + int hoverHeight = r.height(); + + if (hoverViewTop <= 0 && offset > 0) { + smoothScrollBy(-mSmoothScrollAmountAtEdge, 0); + return true; + } + + if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) { + smoothScrollBy(mSmoothScrollAmountAtEdge, 0); + return true; + } + + return false; + } + + public void setList(List list) { + this.mList = list; + } /** + * This scroll listener is added to the listview in order to handle cell swapping + * when the cell is either at the top or bottom edge of the listview. If the hover + * cell is at either edge of the listview, the listview will begin scrolling. As + * scrolling takes place, the listview continuously checks if new cells became visible + * and determines whether they are potential candidates for a cell swap. + */ + private AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener() { + + private int mPreviousFirstVisibleItem = -1; + private int mPreviousVisibleItemCount = -1; + private int mCurrentFirstVisibleItem; + private int mCurrentVisibleItemCount; + private int mCurrentScrollState; + + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + mCurrentFirstVisibleItem = firstVisibleItem; + mCurrentVisibleItemCount = visibleItemCount; + + mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem + : mPreviousFirstVisibleItem; + mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount + : mPreviousVisibleItemCount; + + checkAndHandleFirstVisibleCellChange(); + checkAndHandleLastVisibleCellChange(); + + mPreviousFirstVisibleItem = mCurrentFirstVisibleItem; + mPreviousVisibleItemCount = mCurrentVisibleItemCount; + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mCurrentScrollState = scrollState; + mScrollState = scrollState; + isScrollCompleted(); + } + + /** + * This method is in charge of invoking 1 of 2 actions. Firstly, if the listview + * is in a state of scrolling invoked by the hover cell being outside the bounds + * of the listview, then this scrolling event is continued. Secondly, if the hover + * cell has already been released, this invokes the animation for the hover cell + * to return to its correct position after the listview has entered an idle scroll + * state. + */ + private void isScrollCompleted() { + if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) { + if (mCellIsMobile && mIsMobileScrolling) { + handleMobileCellScroll(); + } else if (mIsWaitingForScrollFinish) { + touchEventsEnded(); + } + } + } + + /** + * Determines if the listview scrolled up enough to reveal a new cell at the + * top of the list. If so, then the appropriate parameters are updated. + */ + public void checkAndHandleFirstVisibleCellChange() { + if (mCurrentFirstVisibleItem != mPreviousFirstVisibleItem) { + if (mCellIsMobile && mMobileItemId != INVALID_ID) { + updateNeighborViewsForID(mMobileItemId); + handleCellSwitch(); + } + } + } + + /** + * Determines if the listview scrolled down enough to reveal a new cell at the + * bottom of the list. If so, then the appropriate parameters are updated. + */ + public void checkAndHandleLastVisibleCellChange() { + int currentLastVisibleItem = mCurrentFirstVisibleItem + mCurrentVisibleItemCount; + int previousLastVisibleItem = mPreviousFirstVisibleItem + mPreviousVisibleItemCount; + if (currentLastVisibleItem != previousLastVisibleItem) { + if (mCellIsMobile && mMobileItemId != INVALID_ID) { + updateNeighborViewsForID(mMobileItemId); + handleCellSwitch(); + } + } + } + }; + + public static class StableArrayAdapter extends ArrayAdapter { + + final int INVALID_ID = -1; + + HashMap mIdMap = new HashMap(); + + public StableArrayAdapter(Context context, int rootViewResourceId, int textViewResourceId, List objects) { + super(context, rootViewResourceId, textViewResourceId, objects); + init(objects); + } + + private void init(List objects) { + for (int i = 0; i < objects.size(); ++i) { + mIdMap.put(objects.get(i), i); + } + } + + @Override + public long getItemId(int position) { + if (position < 0 || position >= mIdMap.size()) { + return INVALID_ID; + } + ServiceInfo item = getItem(position); + return mIdMap.get(item); + } + + @Override + public boolean hasStableIds() { + return true; + } + } + + +} \ No newline at end of file diff --git a/src/org/microg/nlp/ui/LocationBackendConfig.java b/src/org/microg/nlp/ui/LocationBackendConfig.java index 65c69e7..ba738ae 100644 --- a/src/org/microg/nlp/ui/LocationBackendConfig.java +++ b/src/org/microg/nlp/ui/LocationBackendConfig.java @@ -1,84 +1,210 @@ package org.microg.nlp.ui; -import android.app.ListActivity; +import android.app.Activity; import android.content.Intent; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.*; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; +import org.microg.nlp.R; import org.microg.nlp.api.NlpApiConstants; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; -public class LocationBackendConfig extends ListActivity { +public class LocationBackendConfig extends Activity { + private static final String TAG = LocationBackendConfig.class.getName(); - private List activeBackends; + private List activeBackends; + private Map knownBackends; + private List unusedBackends; private Adapter adapter; + private DynamicListView listView; + private View addButton; + private PopupMenu popUp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - activeBackends = new ArrayList(); - List backends = new ArrayList(); + setContentView(R.layout.pluginselection); + listView = (DynamicListView) findViewById(android.R.id.list); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView view, View view2, int i, long l) { + Log.d(TAG, "onItemClick: " + l); + } + }); + addButton = findViewById(android.R.id.button1); + addButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showAddPluginPopup(view); + } + }); + + updateBackends(); + } + + private void updateBackends() { + activeBackends = new ArrayList(); + knownBackends = new HashMap(); + unusedBackends = new ArrayList(); Intent intent = new Intent(NlpApiConstants.ACTION_LOCATION_BACKEND); - List resolveInfos = getPackageManager().queryIntentServices(intent, 0); + List resolveInfos = getPackageManager().queryIntentServices(intent, PackageManager.GET_META_DATA); for (ResolveInfo info : resolveInfos) { - String packageName = info.serviceInfo.packageName; - String simpleName = String.valueOf(info.serviceInfo.loadLabel(getPackageManager())); - Drawable icon = info.serviceInfo.loadIcon(getPackageManager()); - backends.add(new KnownBackend(packageName, simpleName, icon)); + ServiceInfo serviceInfo = info.serviceInfo; + String simpleName = String.valueOf(serviceInfo.loadLabel(getPackageManager())); + Drawable icon = serviceInfo.loadIcon(getPackageManager()); + knownBackends.put(serviceInfo, new KnownBackend(serviceInfo, simpleName, icon)); } - adapter = new Adapter(backends); - setListAdapter(adapter); + updateAddButton(); } - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - KnownBackend backend = adapter.getItem(position); - if (activeBackends.contains(backend.packageName)) { - activeBackends.remove(backend.packageName); + private void updateAddButton() { + if (activeBackends.size() == knownBackends.size()) { + if (activeBackends.isEmpty()) { + // No backend installed + // TODO: notify user about that + } + addButton.setVisibility(View.GONE); } else { - activeBackends.add(backend.packageName); + addButton.setVisibility(View.VISIBLE); } - adapter.notifyDataSetChanged(); } - private class Adapter extends ArrayAdapter { - public Adapter(List backends) { - super(LocationBackendConfig.this, android.R.layout.select_dialog_multichoice, android.R.id.text1, backends); + private void resetAdapter() { + List backends = activeBackends; + adapter = new Adapter(backends); + listView.setList(backends); + listView.setAdapter(adapter); + } + + private void updateUnusedBackends() { + unusedBackends.clear(); + for (KnownBackend backend : knownBackends.values()) { + if (!activeBackends.contains(backend.serviceInfo)) { + unusedBackends.add(backend.serviceInfo); + } + } + } + + private void showAddPluginPopup(View anchorView) { + updateUnusedBackends(); + + if (popUp != null) { + popUp.dismiss(); + } + popUp = new PopupMenu(this, anchorView); + + for (int i = 0; i < unusedBackends.size(); i++) { + KnownBackend backend = knownBackends.get(unusedBackends.get(i)); + String label = backend.simpleName; + if (TextUtils.isEmpty(label)) { + label = backend.serviceInfo.name; + } + popUp.getMenu().add(Menu.NONE, i, Menu.NONE, label); + } + popUp.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + popUp.dismiss(); + popUp = null; + + activeBackends.add(unusedBackends.get(menuItem.getItemId())); + updateAddButton(); + resetAdapter(); + return true; + } + }); + popUp.show(); + } + + private Intent createSettingsIntent(ComponentInfo componentInfo) { + Intent settingsIntent = new Intent(Intent.ACTION_VIEW); + settingsIntent.setPackage(componentInfo.packageName); + settingsIntent.setClassName(componentInfo.packageName, + componentInfo.metaData.getString(NlpApiConstants.METADATA_BACKEND_SETTINGS_ACTIVITY)); + return settingsIntent; + } + + private void showSettingsPopup(View anchorView, final KnownBackend backend) { + if (popUp != null) { + popUp.dismiss(); + } + popUp = new PopupMenu(this, anchorView); + popUp.getMenu().add(Menu.NONE, 0, Menu.NONE, "Remove"); // TODO label + if (backend.serviceInfo.metaData != null && + backend.serviceInfo.metaData.getString(NlpApiConstants.METADATA_BACKEND_SETTINGS_ACTIVITY) != null) { + if (getPackageManager().resolveActivity(createSettingsIntent(backend.serviceInfo), 0) != null) { + popUp.getMenu().add(Menu.NONE, 1, Menu.NONE, "Settings"); // TODO label + } + } + popUp.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + popUp.dismiss(); + popUp = null; + + if (item.getItemId() == 0) { + activeBackends.remove(backend.serviceInfo); + updateAddButton(); + resetAdapter(); + } else if (item.getItemId() == 1) { + startActivity(createSettingsIntent(backend.serviceInfo)); + } + return true; + } + }); + popUp.show(); + } + + private class Adapter extends DynamicListView.StableArrayAdapter { + public Adapter(List backends) { + super(LocationBackendConfig.this, R.layout.backend_list_entry, android.R.id.text2, backends); } @Override public View getView(int position, View convertView, ViewGroup parent) { - // User super class to create the View View v = super.getView(position, convertView, parent); - CheckedTextView tv = (CheckedTextView) v.findViewById(android.R.id.text1); - - // Put the image on the TextView - tv.setCompoundDrawablesWithIntrinsicBounds(getItem(position).icon, null, - null, null); - - // Add margin between image and text (support various screen densities) - int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f); - tv.setCompoundDrawablePadding(dp10); - - tv.setChecked(activeBackends.contains(getItem(position).packageName)); - + final KnownBackend backend = knownBackends.get(getItem(position)); + ImageView icon = (ImageView) v.findViewById(android.R.id.icon); + icon.setImageDrawable(backend.icon); + TextView title = (TextView) v.findViewById(android.R.id.text1); + title.setText(backend.simpleName); + TextView subtitle = (TextView) v.findViewById(android.R.id.text2); + subtitle.setText(backend.serviceInfo.name); + View overflow = v.findViewById(android.R.id.button1); + overflow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showSettingsPopup(view, backend); + } + }); return v; } } private class KnownBackend { - private String packageName; + private ServiceInfo serviceInfo; private String simpleName; private Drawable icon; - public KnownBackend(String packageName, String simpleName, Drawable icon) { - this.packageName = packageName; + private KnownBackend(ServiceInfo serviceInfo, String simpleName, Drawable icon) { + this.serviceInfo = serviceInfo; this.simpleName = simpleName; this.icon = icon; }