Added Android code
[wl-app.git] / Android / webViewMarker / src / main / java / com / bossturban / webviewmarker / TextSelectionSupport.java
1 /*
2  * Copyright (C) 2012 - 2014 Brandon Tate, bossturbo
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.bossturban.webviewmarker;
18
19 import org.json.JSONException;
20 import org.json.JSONObject;
21
22 import com.blahti.drag.DragController;
23 import com.blahti.drag.DragController.DragBehavior;
24 import com.blahti.drag.DragLayer;
25 import com.blahti.drag.DragListener;
26 import com.blahti.drag.DragSource;
27 import com.blahti.drag.MyAbsoluteLayout;
28 import com.bossturban.webviewmarker.R;
29
30 import android.annotation.SuppressLint;
31 import android.app.Activity;
32 import android.content.Context;
33 import android.graphics.Rect;
34 import android.os.Build;
35 import android.util.DisplayMetrics;
36 import android.util.Log;
37 import android.view.Display;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.View.OnLongClickListener;
43 import android.view.ViewGroup;
44 import android.view.WindowManager;
45 import android.webkit.WebSettings;
46 import android.webkit.WebView;
47 import android.widget.ImageView;
48
49 import java.util.Locale;
50
51 @SuppressLint("DefaultLocale")
52 public class TextSelectionSupport implements TextSelectionControlListener, OnTouchListener, OnLongClickListener, DragListener {
53     public interface SelectionListener {
54         void startSelection();
55         void selectionChanged(String text);
56         void endSelection();
57     }
58
59     private enum HandleType {
60         START,
61         END,
62         UNKNOWN
63     }
64     private static final String TAG = "SelectionSupport";
65     private static final float CENTERING_SHORTER_MARGIN_RATIO = 12.0f / 48.0f;
66     private static final int JACK_UP_PADDING = 2;
67     private static final int SCROLLING_THRESHOLD = 10;
68
69     private Activity mActivity;
70     private WebView mWebView;
71     private SelectionListener mSelectionListener;
72     private DragLayer mSelectionDragLayer;
73     private DragController mDragController;
74     private ImageView mStartSelectionHandle;
75     private ImageView mEndSelectionHandle;
76     private Rect mSelectionBounds = null;
77     private final Rect mSelectionBoundsTemp = new Rect();
78     private TextSelectionController mSelectionController = null;
79     private int mContentWidth = 0;
80     private HandleType mLastTouchedSelectionHandle = HandleType.UNKNOWN;
81     private boolean mScrolling = false;
82     private float mScrollDiffY = 0;
83     private float mLastTouchY = 0;
84     private float mScrollDiffX = 0;
85     private float mLastTouchX = 0;
86     private float mScale = 1.0f;
87
88     private Runnable mStartSelectionModeHandler = new Runnable() {
89         public void run() {
90             if (mSelectionBounds != null) {
91                 mWebView.addView(mSelectionDragLayer);
92                 drawSelectionHandles();
93                 final int contentHeight = (int)Math.ceil(getDensityDependentValue(mWebView.getContentHeight(), mActivity));
94                 final int contentWidth = mWebView.getWidth();
95                 ViewGroup.LayoutParams layerParams = mSelectionDragLayer.getLayoutParams();
96                 layerParams.height = contentHeight;
97                 layerParams.width = Math.max(contentWidth, mContentWidth);
98                 mSelectionDragLayer.setLayoutParams(layerParams);
99                 if (mSelectionListener != null) {
100                     mSelectionListener.startSelection();
101                 }
102             }
103         }
104     };
105     private Runnable endSelectionModeHandler = new Runnable(){
106         public void run() {
107             mWebView.removeView(mSelectionDragLayer);
108             mSelectionBounds = null;
109             mLastTouchedSelectionHandle = HandleType.UNKNOWN;
110             mWebView.loadUrl("javascript: android.selection.clearSelection();");
111             if (mSelectionListener != null) {
112                 mSelectionListener.endSelection();
113             }
114         }
115     };
116
117     private TextSelectionSupport(Activity activity, WebView webview) {
118         mActivity = activity;
119         mWebView = webview;
120     }
121     public static TextSelectionSupport support(Activity activity, WebView webview) {
122         final TextSelectionSupport selectionSupport = new TextSelectionSupport(activity, webview);
123         selectionSupport.setup();
124         return selectionSupport;
125     }
126
127     public void onScaleChanged(float oldScale, float newScale) {
128         mScale = newScale;
129     }
130     public void setSelectionListener(SelectionListener listener) {
131         mSelectionListener = listener;
132     }
133
134     //
135     // Interfaces of TextSelectionControlListener
136     //
137     @Override
138     public void jsError(String error) {
139         Log.e(TAG, "JSError: " + error);
140     }
141     @Override
142     public void jsLog(String message) {
143         Log.d(TAG, "JSLog: " + message);
144     }
145     @Override
146     public void startSelectionMode() {
147         mActivity.runOnUiThread(mStartSelectionModeHandler);
148     }
149     @Override
150     public void endSelectionMode() {
151         mActivity.runOnUiThread(endSelectionModeHandler);
152     }
153     @Override
154     public void setContentWidth(float contentWidth){
155         mContentWidth = (int)getDensityDependentValue(contentWidth, mActivity);
156     }
157     @Override
158     public void selectionChanged(String range, String text, String handleBounds, boolean isReallyChanged){
159         final Context ctx = mActivity;
160         try {
161             final JSONObject selectionBoundsObject = new JSONObject(handleBounds);
162             final float scale = getDensityIndependentValue(mScale, ctx);
163             Rect rect = mSelectionBoundsTemp;
164             rect.left = (int)(getDensityDependentValue(selectionBoundsObject.getInt("left"), ctx) * scale);
165             rect.top = (int)(getDensityDependentValue(selectionBoundsObject.getInt("top"), ctx) * scale);
166             rect.right = (int)(getDensityDependentValue(selectionBoundsObject.getInt("right"), ctx) * scale);
167             rect.bottom = (int)(getDensityDependentValue(selectionBoundsObject.getInt("bottom"), ctx) * scale);
168             mSelectionBounds = rect;
169             if (!isInSelectionMode()){
170                 startSelectionMode();
171             }
172             drawSelectionHandles();
173             if (mSelectionListener != null && isReallyChanged) {
174                 mSelectionListener.selectionChanged(text);
175             }
176         }
177         catch (JSONException e) {
178             e.printStackTrace();
179         }
180     }
181
182     //
183     // Interface of OnTouchListener
184     //
185     @Override
186     public boolean onTouch(View v, MotionEvent event) {
187         final Context ctx = mActivity;
188         float xPoint = getDensityIndependentValue(event.getX(), ctx) / getDensityIndependentValue(mScale, ctx);
189         float yPoint = getDensityIndependentValue(event.getY(), ctx) / getDensityIndependentValue(mScale, ctx);
190
191         switch (event.getAction()) {
192         case MotionEvent.ACTION_DOWN:
193             // Essential to add Locale.US parameter to String.format, else does not work on systems
194             // with default locale different, with other floating point notations, e.g. comma instead
195             // of decimal point.
196             final String startTouchUrl = String.format(Locale.US, "javascript:android.selection.startTouch(%f, %f);", xPoint, yPoint);
197             mLastTouchX = xPoint;
198             mLastTouchY = yPoint;
199             mWebView.loadUrl(startTouchUrl);
200             break;
201         case MotionEvent.ACTION_UP:
202             if (!mScrolling) {
203                 endSelectionMode();
204                 //
205                 // Fixes 4.4 double selection
206                 // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
207                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
208                     return false;
209                 }
210             }
211             mScrollDiffX = 0;
212             mScrollDiffY = 0;
213             mScrolling = false;
214             //
215             // Fixes 4.4 double selection
216             // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
217             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isInSelectionMode()) {
218                 return true;
219             }
220             break;
221         case MotionEvent.ACTION_MOVE:
222             mScrollDiffX += (xPoint - mLastTouchX);
223             mScrollDiffY += (yPoint - mLastTouchY);
224             mLastTouchX = xPoint;
225             mLastTouchY = yPoint;
226             if (Math.abs(mScrollDiffX) > SCROLLING_THRESHOLD || Math.abs(mScrollDiffY) > SCROLLING_THRESHOLD) {
227                 mScrolling = true;
228             }
229             break;
230         }
231         return false;
232     }
233
234     //
235     // Interface of OnLongClickListener
236     //
237     @Override 
238     public boolean onLongClick(View v){
239         if (!isInSelectionMode()) {
240             mWebView.loadUrl("javascript:android.selection.longTouch();");
241             mScrolling = true;
242         }
243         return true;
244     }
245
246     //
247     // Interface of DragListener
248     //
249     @Override
250     public void onDragStart(DragSource source, Object info, DragBehavior dragBehavior) {
251     }
252     @Override
253     public void onDragEnd() {
254         mActivity.runOnUiThread(new Runnable() {
255             @Override
256             public void run() {
257                 MyAbsoluteLayout.LayoutParams startHandleParams = (MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
258                 MyAbsoluteLayout.LayoutParams endHandleParams = (MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
259                 final Context ctx = mActivity;
260                 final float scale = getDensityIndependentValue(mScale, ctx);
261                 float startX = startHandleParams.x - mWebView.getScrollX() + mStartSelectionHandle.getWidth() * (1 - CENTERING_SHORTER_MARGIN_RATIO);
262                 float startY = startHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
263                 float endX = endHandleParams.x - mWebView.getScrollX() + mEndSelectionHandle.getWidth() * CENTERING_SHORTER_MARGIN_RATIO;
264                 float endY = endHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
265                 startX = getDensityIndependentValue(startX, ctx) / scale;
266                 startY = getDensityIndependentValue(startY, ctx) / scale;
267                 endX = getDensityIndependentValue(endX, ctx) / scale;
268                 endY = getDensityIndependentValue(endY, ctx) / scale;
269                 if (mLastTouchedSelectionHandle == HandleType.START && startX > 0 && startY > 0){
270                     String saveStartString = String.format(Locale.US, "javascript: android.selection.setStartPos(%f, %f);", startX, startY);
271                     mWebView.loadUrl(saveStartString);
272                 }
273                 else if (mLastTouchedSelectionHandle == HandleType.END && endX > 0 && endY > 0){
274                     String saveEndString = String.format(Locale.US, "javascript: android.selection.setEndPos(%f, %f);", endX, endY);
275                     mWebView.loadUrl(saveEndString);
276                 }
277                 else {
278                     mWebView.loadUrl("javascript: android.selection.restoreStartEndPos();");
279                 }
280             }
281         });
282     }
283
284     @SuppressLint("SetJavaScriptEnabled")
285     private void setup(){
286         mScale = mActivity.getResources().getDisplayMetrics().density;
287         mWebView.setOnLongClickListener(this);
288         mWebView.setOnTouchListener(this);
289         final WebSettings settings = mWebView.getSettings();
290         settings.setJavaScriptEnabled(true);
291         settings.setJavaScriptCanOpenWindowsAutomatically(true);
292         mSelectionController = new TextSelectionController(this);
293         mWebView.addJavascriptInterface(mSelectionController, TextSelectionController.INTERFACE_NAME);
294         createSelectionLayer(mActivity);
295     }
296     private void createSelectionLayer(Context context){
297         final LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
298         mSelectionDragLayer = (DragLayer)inflater.inflate(R.layout.selection_drag_layer, null);
299         mDragController = new DragController(context);
300         mDragController.setDragListener(this);
301         mDragController.addDropTarget(mSelectionDragLayer);
302         mSelectionDragLayer.setDragController(mDragController);
303         mStartSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.startHandle);
304         mStartSelectionHandle.setTag(HandleType.START);
305         mEndSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.endHandle);
306         mEndSelectionHandle.setTag(HandleType.END);
307         final OnTouchListener handleTouchListener = new OnTouchListener(){
308             @Override
309             public boolean onTouch(View v, MotionEvent event) {
310                 boolean handledHere = false;
311                 if (event.getAction() == MotionEvent.ACTION_DOWN) {
312                     handledHere = startDrag(v);
313                     mLastTouchedSelectionHandle = (HandleType)v.getTag();
314                 }
315                 return handledHere;
316             }
317         };
318         mStartSelectionHandle.setOnTouchListener(handleTouchListener);
319         mEndSelectionHandle.setOnTouchListener(handleTouchListener);
320     }
321     private void drawSelectionHandles(){
322         mActivity.runOnUiThread(drawSelectionHandlesHandler);
323     }
324     private Runnable drawSelectionHandlesHandler = new Runnable(){
325         public void run() {
326             MyAbsoluteLayout.LayoutParams startParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
327             final int startWidth = mStartSelectionHandle.getDrawable().getIntrinsicWidth();
328             startParams.x = (int)(mSelectionBounds.left - startWidth * (1.0f - CENTERING_SHORTER_MARGIN_RATIO));
329             startParams.y = (int)(mSelectionBounds.top);
330             final int startMinLeft = -(int)(startWidth * (1 - CENTERING_SHORTER_MARGIN_RATIO));
331             startParams.x = (startParams.x < startMinLeft) ? startMinLeft : startParams.x;
332             startParams.y = (startParams.y < 0) ? 0 : startParams.y;
333             mStartSelectionHandle.setLayoutParams(startParams);
334
335             MyAbsoluteLayout.LayoutParams endParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
336             final int endWidth = mEndSelectionHandle.getDrawable().getIntrinsicWidth();
337             endParams.x = (int) (mSelectionBounds.right - endWidth * CENTERING_SHORTER_MARGIN_RATIO);
338             endParams.y = (int) (mSelectionBounds.bottom);
339             final int endMinLeft = -(int)(endWidth * (1- CENTERING_SHORTER_MARGIN_RATIO));
340             endParams.x = (endParams.x < endMinLeft) ? endMinLeft : endParams.x;
341             endParams.y = (endParams.y < 0) ? 0 : endParams.y;
342             mEndSelectionHandle.setLayoutParams(endParams);
343         }
344     };
345
346     private boolean isInSelectionMode(){
347         return this.mSelectionDragLayer.getParent() != null;
348     }
349     private boolean startDrag(View v) {
350         Object dragInfo = v;
351         mDragController.startDrag(v, mSelectionDragLayer, dragInfo, DragBehavior.MOVE);
352         return true;
353     }
354
355     private float getDensityDependentValue(float val, Context ctx){
356         Display display = ((WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
357         DisplayMetrics metrics = new DisplayMetrics();
358         display.getMetrics(metrics);
359         return val * (metrics.densityDpi / 160f);
360     }
361     private float getDensityIndependentValue(float val, Context ctx){
362         Display display = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
363         DisplayMetrics metrics = new DisplayMetrics();
364         display.getMetrics(metrics);
365         return val / (metrics.densityDpi / 160f);
366     }
367 }