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 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 106 static GestureLibrary getStore() { 107 return sStore; 108 } 109 110 @SuppressWarnings({"UnusedDeclaration"}) 111 public void reloadGestures(View v) { 112 loadGestures(); 113 } 114 115 @SuppressWarnings({"UnusedDeclaration"}) 116 public void addGesture(View v) { 117 Intent intent = new Intent(this, CreateGestureActivity.class); 118 startActivityForResult(intent, REQUEST_NEW_GESTURE); 119 } 120 121 @Override 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 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 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 153 private void checkForEmpty() { 154 if (mAdapter.getCount() == 0) { 155 mEmpty.setText(R.string.gestures_empty); 156 } 157 } 158 159 @Override 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 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 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 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 219 private void renameGesture(NamedGesture gesture) { 220 mCurrentRenameGesture = gesture; 221 showDialog(DIALOG_RENAME_GESTURE); 222 } 223 224 @Override 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 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 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 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 296 private void cleanupRenameDialog() { 297 if (mRenameDialog != null) { 298 mRenameDialog.dismiss(); 299 mRenameDialog = null; 300 } 301 mCurrentRenameGesture = null; 302 } 303 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 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 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 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 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 412 public GesturesAdapter(Context context) { 413 super(context, 0); 414 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 415 } 416 417 void addBitmap(Long id, Bitmap bitmap) { 418 mThumbnails.put(id, new BitmapDrawable(bitmap)); 419 } 420 421 @Override 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