1 /*
2  * Copyright (C) 2009 The Android Open Source Project
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.android.gesture.builder;
18 
19 import android.app.Dialog;
20 import android.app.AlertDialog;
21 import android.app.ListActivity;
22 import android.os.Bundle;
23 import android.os.AsyncTask;
24 import android.os.Environment;
25 import android.view.View;
26 import android.view.ContextMenu;
27 import android.view.MenuItem;
28 import android.view.LayoutInflater;
29 import android.view.ViewGroup;
30 import android.gesture.GestureLibrary;
31 import android.gesture.Gesture;
32 import android.gesture.GestureLibraries;
33 import android.widget.TextView;
34 import android.widget.EditText;
35 import android.widget.AdapterView;
36 import android.widget.Toast;
37 import android.widget.ArrayAdapter;
38 import android.content.DialogInterface;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.res.Resources;
42 import android.text.TextUtils;
43 import android.graphics.Bitmap;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.BitmapDrawable;
46 
47 import java.util.Map;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.Comparator;
51 import java.util.Set;
52 import java.io.File;
53 
54 public class GestureBuilderActivity extends ListActivity {
55     private static final int STATUS_SUCCESS = 0;
56     private static final int STATUS_CANCELLED = 1;
57     private static final int STATUS_NO_STORAGE = 2;
58     private static final int STATUS_NOT_LOADED = 3;
59 
60     private static final int MENU_ID_RENAME = 1;
61     private static final int MENU_ID_REMOVE = 2;
62 
63     private static final int DIALOG_RENAME_GESTURE = 1;
64 
65     private static final int REQUEST_NEW_GESTURE = 1;
66 
67     // Type: long (id)
68     private static final String GESTURES_INFO_ID = "gestures.info_id";
69 
70     private final File mStoreFile = new File(Environment.getExternalStorageDirectory(), "gestures");
71 
72     private final Comparator<NamedGesture> mSorter = new Comparator<NamedGesture>() {
73         public int compare(NamedGesture object1, NamedGesture object2) {
74             return object1.name.compareTo(object2.name);
75         }
76     };
77 
78     private static GestureLibrary sStore;
79 
80     private GesturesAdapter mAdapter;
81     private GesturesLoadTask mTask;
82     private TextView mEmpty;
83 
84     private Dialog mRenameDialog;
85     private EditText mInput;
86     private NamedGesture mCurrentRenameGesture;
87 
88     @Override
onCreate(Bundle savedInstanceState)89     protected void onCreate(Bundle savedInstanceState) {
90         super.onCreate(savedInstanceState);
91 
92         setContentView(R.layout.gestures_list);
93 
94         mAdapter = new GesturesAdapter(this);
95         setListAdapter(mAdapter);
96 
97         if (sStore == null) {
98             sStore = GestureLibraries.fromFile(mStoreFile);
99         }
100         mEmpty = (TextView) findViewById(android.R.id.empty);
101         loadGestures();
102 
103         registerForContextMenu(getListView());
104     }
105 
getStore()106     static GestureLibrary getStore() {
107         return sStore;
108     }
109 
110     @SuppressWarnings({"UnusedDeclaration"})
reloadGestures(View v)111     public void reloadGestures(View v) {
112         loadGestures();
113     }
114 
115     @SuppressWarnings({"UnusedDeclaration"})
addGesture(View v)116     public void addGesture(View v) {
117         Intent intent = new Intent(this, CreateGestureActivity.class);
118         startActivityForResult(intent, REQUEST_NEW_GESTURE);
119     }
120 
121     @Override
onActivityResult(int requestCode, int resultCode, Intent data)122     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
123         super.onActivityResult(requestCode, resultCode, data);
124 
125         if (resultCode == RESULT_OK) {
126             switch (requestCode) {
127                 case REQUEST_NEW_GESTURE:
128                     loadGestures();
129                     break;
130             }
131         }
132     }
133 
loadGestures()134     private void loadGestures() {
135         if (mTask != null && mTask.getStatus() != GesturesLoadTask.Status.FINISHED) {
136             mTask.cancel(true);
137         }
138         mTask = (GesturesLoadTask) new GesturesLoadTask().execute();
139     }
140 
141     @Override
onDestroy()142     protected void onDestroy() {
143         super.onDestroy();
144 
145         if (mTask != null && mTask.getStatus() != GesturesLoadTask.Status.FINISHED) {
146             mTask.cancel(true);
147             mTask = null;
148         }
149 
150         cleanupRenameDialog();
151     }
152 
checkForEmpty()153     private void checkForEmpty() {
154         if (mAdapter.getCount() == 0) {
155             mEmpty.setText(R.string.gestures_empty);
156         }
157     }
158 
159     @Override
onSaveInstanceState(Bundle outState)160     protected void onSaveInstanceState(Bundle outState) {
161         super.onSaveInstanceState(outState);
162 
163         if (mCurrentRenameGesture != null) {
164             outState.putLong(GESTURES_INFO_ID, mCurrentRenameGesture.gesture.getID());
165         }
166     }
167 
168     @Override
onRestoreInstanceState(Bundle state)169     protected void onRestoreInstanceState(Bundle state) {
170         super.onRestoreInstanceState(state);
171 
172         long id = state.getLong(GESTURES_INFO_ID, -1);
173         if (id != -1) {
174             final Set<String> entries = sStore.getGestureEntries();
175 out:        for (String name : entries) {
176                 for (Gesture gesture : sStore.getGestures(name)) {
177                     if (gesture.getID() == id) {
178                         mCurrentRenameGesture = new NamedGesture();
179                         mCurrentRenameGesture.name = name;
180                         mCurrentRenameGesture.gesture = gesture;
181                         break out;
182                     }
183                 }
184             }
185         }
186     }
187 
188     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)189     public void onCreateContextMenu(ContextMenu menu, View v,
190             ContextMenu.ContextMenuInfo menuInfo) {
191 
192         super.onCreateContextMenu(menu, v, menuInfo);
193 
194         AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
195         menu.setHeaderTitle(((TextView) info.targetView).getText());
196 
197         menu.add(0, MENU_ID_RENAME, 0, R.string.gestures_rename);
198         menu.add(0, MENU_ID_REMOVE, 0, R.string.gestures_delete);
199     }
200 
201     @Override
onContextItemSelected(MenuItem item)202     public boolean onContextItemSelected(MenuItem item) {
203         final AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo)
204                 item.getMenuInfo();
205         final NamedGesture gesture = (NamedGesture) menuInfo.targetView.getTag();
206 
207         switch (item.getItemId()) {
208             case MENU_ID_RENAME:
209                 renameGesture(gesture);
210                 return true;
211             case MENU_ID_REMOVE:
212                 deleteGesture(gesture);
213                 return true;
214         }
215 
216         return super.onContextItemSelected(item);
217     }
218 
renameGesture(NamedGesture gesture)219     private void renameGesture(NamedGesture gesture) {
220         mCurrentRenameGesture = gesture;
221         showDialog(DIALOG_RENAME_GESTURE);
222     }
223 
224     @Override
onCreateDialog(int id)225     protected Dialog onCreateDialog(int id) {
226         if (id == DIALOG_RENAME_GESTURE) {
227             return createRenameDialog();
228         }
229         return super.onCreateDialog(id);
230     }
231 
232     @Override
onPrepareDialog(int id, Dialog dialog)233     protected void onPrepareDialog(int id, Dialog dialog) {
234         super.onPrepareDialog(id, dialog);
235         if (id == DIALOG_RENAME_GESTURE) {
236             mInput.setText(mCurrentRenameGesture.name);
237         }
238     }
239 
createRenameDialog()240     private Dialog createRenameDialog() {
241         final View layout = View.inflate(this, R.layout.dialog_rename, null);
242         mInput = (EditText) layout.findViewById(R.id.name);
243         ((TextView) layout.findViewById(R.id.label)).setText(R.string.gestures_rename_label);
244 
245         AlertDialog.Builder builder = new AlertDialog.Builder(this);
246         builder.setIcon(0);
247         builder.setTitle(getString(R.string.gestures_rename_title));
248         builder.setCancelable(true);
249         builder.setOnCancelListener(new Dialog.OnCancelListener() {
250             public void onCancel(DialogInterface dialog) {
251                 cleanupRenameDialog();
252             }
253         });
254         builder.setNegativeButton(getString(R.string.cancel_action),
255             new Dialog.OnClickListener() {
256                 public void onClick(DialogInterface dialog, int which) {
257                     cleanupRenameDialog();
258                 }
259             }
260         );
261         builder.setPositiveButton(getString(R.string.rename_action),
262             new Dialog.OnClickListener() {
263                 public void onClick(DialogInterface dialog, int which) {
264                     changeGestureName();
265                 }
266             }
267         );
268         builder.setView(layout);
269         return builder.create();
270     }
271 
changeGestureName()272     private void changeGestureName() {
273         final String name = mInput.getText().toString();
274         if (!TextUtils.isEmpty(name)) {
275             final NamedGesture renameGesture = mCurrentRenameGesture;
276             final GesturesAdapter adapter = mAdapter;
277             final int count = adapter.getCount();
278 
279             // Simple linear search, there should not be enough items to warrant
280             // a more sophisticated search
281             for (int i = 0; i < count; i++) {
282                 final NamedGesture gesture = adapter.getItem(i);
283                 if (gesture.gesture.getID() == renameGesture.gesture.getID()) {
284                     sStore.removeGesture(gesture.name, gesture.gesture);
285                     gesture.name = mInput.getText().toString();
286                     sStore.addGesture(gesture.name, gesture.gesture);
287                     break;
288                 }
289             }
290 
291             adapter.notifyDataSetChanged();
292         }
293         mCurrentRenameGesture = null;
294     }
295 
cleanupRenameDialog()296     private void cleanupRenameDialog() {
297         if (mRenameDialog != null) {
298             mRenameDialog.dismiss();
299             mRenameDialog = null;
300         }
301         mCurrentRenameGesture = null;
302     }
303 
deleteGesture(NamedGesture gesture)304     private void deleteGesture(NamedGesture gesture) {
305         sStore.removeGesture(gesture.name, gesture.gesture);
306         sStore.save();
307 
308         final GesturesAdapter adapter = mAdapter;
309         adapter.setNotifyOnChange(false);
310         adapter.remove(gesture);
311         adapter.sort(mSorter);
312         checkForEmpty();
313         adapter.notifyDataSetChanged();
314 
315         Toast.makeText(this, R.string.gestures_delete_success, Toast.LENGTH_SHORT).show();
316     }
317 
318     private class GesturesLoadTask extends AsyncTask<Void, NamedGesture, Integer> {
319         private int mThumbnailSize;
320         private int mThumbnailInset;
321         private int mPathColor;
322 
323         @Override
onPreExecute()324         protected void onPreExecute() {
325             super.onPreExecute();
326 
327             final Resources resources = getResources();
328             mPathColor = resources.getColor(R.color.gesture_color);
329             mThumbnailInset = (int) resources.getDimension(R.dimen.gesture_thumbnail_inset);
330             mThumbnailSize = (int) resources.getDimension(R.dimen.gesture_thumbnail_size);
331 
332             findViewById(R.id.addButton).setEnabled(false);
333             findViewById(R.id.reloadButton).setEnabled(false);
334 
335             mAdapter.setNotifyOnChange(false);
336             mAdapter.clear();
337         }
338 
339         @Override
doInBackground(Void... params)340         protected Integer doInBackground(Void... params) {
341             if (isCancelled()) return STATUS_CANCELLED;
342             if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
343                 return STATUS_NO_STORAGE;
344             }
345 
346             final GestureLibrary store = sStore;
347 
348             if (store.load()) {
349                 for (String name : store.getGestureEntries()) {
350                     if (isCancelled()) break;
351 
352                     for (Gesture gesture : store.getGestures(name)) {
353                         final Bitmap bitmap = gesture.toBitmap(mThumbnailSize, mThumbnailSize,
354                                 mThumbnailInset, mPathColor);
355                         final NamedGesture namedGesture = new NamedGesture();
356                         namedGesture.gesture = gesture;
357                         namedGesture.name = name;
358 
359                         mAdapter.addBitmap(namedGesture.gesture.getID(), bitmap);
360                         publishProgress(namedGesture);
361                     }
362                 }
363 
364                 return STATUS_SUCCESS;
365             }
366 
367             return STATUS_NOT_LOADED;
368         }
369 
370         @Override
onProgressUpdate(NamedGesture... values)371         protected void onProgressUpdate(NamedGesture... values) {
372             super.onProgressUpdate(values);
373 
374             final GesturesAdapter adapter = mAdapter;
375             adapter.setNotifyOnChange(false);
376 
377             for (NamedGesture gesture : values) {
378                 adapter.add(gesture);
379             }
380 
381             adapter.sort(mSorter);
382             adapter.notifyDataSetChanged();
383         }
384 
385         @Override
onPostExecute(Integer result)386         protected void onPostExecute(Integer result) {
387             super.onPostExecute(result);
388 
389             if (result == STATUS_NO_STORAGE) {
390                 getListView().setVisibility(View.GONE);
391                 mEmpty.setVisibility(View.VISIBLE);
392                 mEmpty.setText(getString(R.string.gestures_error_loading,
393                         mStoreFile.getAbsolutePath()));
394             } else {
395                 findViewById(R.id.addButton).setEnabled(true);
396                 findViewById(R.id.reloadButton).setEnabled(true);
397                 checkForEmpty();
398             }
399         }
400     }
401 
402     static class NamedGesture {
403         String name;
404         Gesture gesture;
405     }
406 
407     private class GesturesAdapter extends ArrayAdapter<NamedGesture> {
408         private final LayoutInflater mInflater;
409         private final Map<Long, Drawable> mThumbnails = Collections.synchronizedMap(
410                 new HashMap<Long, Drawable>());
411 
GesturesAdapter(Context context)412         public GesturesAdapter(Context context) {
413             super(context, 0);
414             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
415         }
416 
addBitmap(Long id, Bitmap bitmap)417         void addBitmap(Long id, Bitmap bitmap) {
418             mThumbnails.put(id, new BitmapDrawable(bitmap));
419         }
420 
421         @Override
getView(int position, View convertView, ViewGroup parent)422         public View getView(int position, View convertView, ViewGroup parent) {
423             if (convertView == null) {
424                 convertView = mInflater.inflate(R.layout.gestures_item, parent, false);
425             }
426 
427             final NamedGesture gesture = getItem(position);
428             final TextView label = (TextView) convertView;
429 
430             label.setTag(gesture);
431             label.setText(gesture.name);
432             label.setCompoundDrawablesWithIntrinsicBounds(mThumbnails.get(gesture.gesture.getID()),
433                     null, null, null);
434 
435             return convertView;
436         }
437     }
438 }
439