Android笔记之二:改进的NotePad Demo

 下面是对SDK中的NotePad smaple做的一些改进,效果图在最后。

刚开始看Android,这是继hello之后的第二个例子。

不对的地方,请批评指正。

 

java文件有:

1.NotesList.java

2.NoteEditor.java

3.NotePadProvider.java

4.NotePad.java

5.DateUtil.java

 

layout有:

1.note_editor.xml

2.noteslist_item.xml

 

 

贴出来修改之后的代码:

NotesList.java:

package com.cticc.notepad;

import android.app.ListActivity;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;

import com.cticc.notepad.NotePad.Notes;

/**
 * Displays a list of notes. Will display notes from the {@link Uri} provided in the intent if there
 * is one, otherwise defaults to displaying the contents of the {@link NotePadProvider}
 */
public class NotesList extends ListActivity
{
	private static final String TAG = "NotesList";

	public static final int MENU_ITEM_DELETE = Menu.FIRST;
	public static final int MENU_ITEM_INSERT = Menu.FIRST + 1;

	/**
	 * The columns we are interested in from the database
	 */
	private static final String[] PROJECTION = new String[] { Notes._ID, // 0
			Notes.KEY_TITLE, // 1
			Notes.KEY_CREATEAT // 2
	};

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);

		setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);

		// If no data was given in the intent (because we were started as a MAIN activity), then use our default content provider.
		Intent intent = getIntent();
		if (intent.getData() == null)
		{
			intent.setData(Notes.CONTENT_URI);
		}

		// Inform the list we provide context menus for items
		getListView().setOnCreateContextMenuListener(this);

		// Perform a managed query. The Activity will handle closing and requerying the cursor when needed.
		Cursor cursor = managedQuery(getIntent().getData(), PROJECTION, null, null, Notes.DEFAULT_SORT_ORDER);

		// Used to map notes entries from the database to views
		SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor, new String[] { Notes.KEY_CREATEAT, Notes.KEY_TITLE }, new int[] { android.R.id.text1,
				android.R.id.text2 });
		setListAdapter(adapter);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu)
	{
		super.onCreateOptionsMenu(menu);

		Log.i(TAG, "enter in onCreateOptionsMenu");

		// This is our one standard application action -- inserting a new note into the list.
		menu.add(0, MENU_ITEM_INSERT, 0, R.string.menu_insert).setShortcut('3', 'a').setIcon(android.R.drawable.ic_menu_add);

		return true;
	}

	@Override
	public boolean onPrepareOptionsMenu(Menu menu)
	{
		super.onPrepareOptionsMenu(menu);

		Log.i(TAG, "enter in onPrepareOptionsMenu");
		final boolean haveItems = getListAdapter().getCount() > 0;

		// If there are any notes in the list (which implies that one of them is selected), then we need to generate the actions that
		// can be performed on the current selection. This will be a combination of our own specific 
		// actions along with any extensions that can be found.
		if (haveItems)
		{
			// This is the selected item.
			Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId());
			
			// Build menu... always starts with the EDIT action...
			Intent[] specifics = new Intent[1];
			specifics[0] = new Intent(Intent.ACTION_EDIT, uri);
			MenuItem[] items = new MenuItem[1];

			// ... is followed by whatever other actions are available...
			Intent intent = new Intent(null, uri);
			intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
			menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, null, specifics, intent, 0, items);
			
			// Give a shortcut to the edit action.
			if (items[0] != null)
			{
				items[0].setShortcut('1', 'e').setIcon(android.R.drawable.ic_menu_edit);
			}
		}
		else
		{
			menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);
		}

		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item)
	{
		switch (item.getItemId())
		{
		case MENU_ITEM_INSERT:
			// Launch activity to insert a new item
			startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));
			return true;
		}
		return super.onOptionsItemSelected(item);
	}
	
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id)
	{
		Uri uri = ContentUris.withAppendedId(getIntent().getData(), id);

		String action = getIntent().getAction();
		if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action))
		{
			// The caller is waiting for us to return a note selected by
			// the user. The have clicked on one, so return it now.
			setResult(RESULT_OK, new Intent().setData(uri));
		}
		else
		{
			// Launch activity to view/edit the currently selected item
			startActivity(new Intent(Intent.ACTION_EDIT, uri));
		}
	}
}

 

 NoteEditor.java:

package com.cticc.notepad;

import com.cticc.notepad.NotePad.Notes;
import com.cticc.notepad.util.DateUtil;

import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;

/**
 * A generic activity for editing a note in a database. This can be used either to simply view a
 * note {@link Intent#ACTION_VIEW}, view and edit a note {@link Intent#ACTION_EDIT}, or create a new
 * note {@link Intent#ACTION_INSERT}.
 */
public class NoteEditor extends Activity
{
	private static final String TAG = "Notes";

	// Standard projection for the interesting columns of a normal note.
	private static final String[] PROJECTION = new String[] { Notes._ID, // 0
			Notes.KEY_TITLE, // 1
			Notes.KEY_CONTENT // 2
	};
	// The index of the note column
	private static final int COLUMN_INDEX_TITLE = 1;
	private static final int COLUMN_INDEX_CONTENT = 2;

	// Identifiers for our menu items.
	private static final int MENU_CONFIRM_ID = Menu.FIRST;
	private static final int MENU_DELETE_ID = Menu.FIRST + 1;
	private static final int MENU_DISCARD_ID = Menu.FIRST + 2;

	// The different distinct states the activity can be run in.
	private static final int ACTION_EDIT = 0;
	private static final int ACTION_INSERT = 1;

	private int mState;
	private boolean mNoteOnly = false;
	private Uri mUri;
	private Cursor mCursor;
	private EditText mContent;
	private EditText mTitle;
	//private String mOriginalContent;

	/**
	 * A custom EditText that draws lines between each line of text that is displayed.
	 */
	public static class LinedEditText extends EditText
	{
		private Rect mRect;
		private Paint mPaint;

		// we need this constructor for LayoutInflater
		public LinedEditText(Context context, AttributeSet attrs)
		{
			super(context, attrs);

			mRect = new Rect();
			mPaint = new Paint();
			mPaint.setStyle(Paint.Style.STROKE);
			mPaint.setColor(0x800000FF);
		}

		@Override
		protected void onDraw(Canvas canvas)
		{
			int count = getLineCount();
			Rect r = mRect;
			Paint paint = mPaint;

			for (int i = 0; i < count; i++)
			{
				int baseline = getLineBounds(i, r);
				canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
			}
			super.onDraw(canvas);
		}
	}

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);

		final Intent intent = getIntent();

		// Do some setup based on the action being performed.

		final String action = intent.getAction();
		if (Intent.ACTION_EDIT.equals(action))
		{
			// Requested to edit: set that state, and the data being edited.
			mState = ACTION_EDIT;
			mUri = intent.getData();
		}
		else if (Intent.ACTION_INSERT.equals(action))
		{
			// Requested to insert: set that state, and create a new entry
			// in the container.
			mState = ACTION_INSERT;
			Log.i(TAG, "intent.getData() is : " + intent.getData());
			//mUri = getContentResolver().insert(intent.getData(), null);
			mUri = intent.getData();
			// If we were unable to create a new note, then just finish
			// this activity. A RESULT_CANCELED will be sent back to the
			// original activity if they requested a result.
			if (mUri == null)
			{
				Log.e(TAG, "Failed to insert new note into " + getIntent().getData());
				finish();
				return;
			}

			// The new entry was created, so assume all will end well and
			// set the result to be returned.
			setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));

		}
		else
		{
			// Whoops, unknown action! Bail.
			Log.e(TAG, "Unknown action, exiting");
			finish();
			return;
		}

		// Set the layout for this activity. You can find it in res/layout/note_editor.xml
		setContentView(R.layout.note_editor);

		// The text view for our note, identified by its ID in the XML file.
		mContent = (EditText) findViewById(R.id.content);
		mTitle = (EditText) findViewById(R.id.title);

		// Get the note!
		mCursor = managedQuery(mUri, PROJECTION, null, null, null);
	}

	@Override
	protected void onResume()
	{
		super.onResume();

		// If we didn't have any trouble retrieving the data, it is now
		// time to get at the stuff.
		if (mCursor != null)
		{
			// Make sure we are at the one and only row in the cursor.
			mCursor.moveToFirst();

			// Modify our overall title depending on the mode we are running in.
			if (mState == ACTION_EDIT)
			{
				setTitle(getText(R.string.label_edit));
				
				String content = mCursor.getString(COLUMN_INDEX_CONTENT);
				String title = mCursor.getString(COLUMN_INDEX_TITLE);
				
				mContent.setTextKeepState(content);
				mTitle.setTextKeepState(title);
			}
			else if (mState == ACTION_INSERT)
			{
				setTitle(getText(R.string.label_insert));
			}
		}
		else
		{
			
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu)
	{
		super.onCreateOptionsMenu(menu);

		menu.add(0, MENU_CONFIRM_ID, 0, R.string.menu_confirm).setShortcut('0', 'c').setIcon(android.R.drawable.ic_menu_save);
		if (mState == ACTION_EDIT)
		{
			if (!mNoteOnly)
			{
				menu.add(0, MENU_DELETE_ID, 0, R.string.menu_delete).setShortcut('1', 'd').setIcon(android.R.drawable.ic_menu_delete);
			}
		}

		menu.add(0, MENU_DISCARD_ID, 0, R.string.menu_discard).setShortcut('2', 'd').setIcon(android.R.drawable.ic_menu_revert);
		
		if (!mNoteOnly)
		{
			Intent intent = new Intent(null, getIntent().getData());
			intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
			menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, new ComponentName(this, NoteEditor.class), null, intent, 0, null);
		}

		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item)
	{
		// Handle all of the possible menu actions.
		switch (item.getItemId())
		{
		case MENU_CONFIRM_ID:
			saveState();
			finish();
			break;
		case MENU_DELETE_ID:
			deleteNote();
			finish();
			break;
		case MENU_DISCARD_ID:
			finish();
			break;
		}
		return super.onOptionsItemSelected(item);
	}

	private void saveState()
	{
		ContentValues values = new ContentValues();

		values.put(Notes.KEY_CONTENT, mContent.getText().toString());
		values.put(Notes.KEY_TITLE, mTitle.getText().toString());
		String sdate = DateUtil.getTodayTimeStampString();

		if (mState == ACTION_INSERT)
		{
			values.put(Notes.KEY_CREATEAT, DateUtil.getToday());
			values.put(Notes.KEY_UPDATEAT, sdate);
			getContentResolver().insert(mUri, values);
		}
		else if (mState == ACTION_EDIT)
		{
			values.put(Notes.KEY_UPDATEAT, sdate);
			getContentResolver().update(mUri, values, null, null);
		}

	}
	/**
	 * Take care of deleting a note. Simply deletes the entry.
	 */
	private final void deleteNote()
	{
		if (mCursor != null)
		{
			mCursor.close();
			mCursor = null;
			getContentResolver().delete(mUri, null, null);
			mContent.setText("");
		}
	}
}

 

 NotePadProvider.java:

package com.cticc.notepad;

import com.cticc.notepad.NotePad.Notes;
import com.cticc.notepad.util.DateUtil;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import java.util.HashMap;

/**
 * Provides access to a database of notes. Each note has a title, the note itself, a creation date and a modified data.
 */
public class NotePadProvider extends ContentProvider
{

	private static final String TAG = "NotePadProvider";

	private SQLiteDatabase sqlitedb;

	private static HashMap sNotesProjectionMap;

	private static final int NOTES = 1;
	private static final int NOTE_ID = 2;

	private static final UriMatcher sUriMatcher;
	
	static
	{
		sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
		sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);

		sNotesProjectionMap = new HashMap();
		sNotesProjectionMap.put(Notes._ID, Notes._ID);
		sNotesProjectionMap.put(Notes.KEY_TITLE, Notes.KEY_TITLE);
		sNotesProjectionMap.put(Notes.KEY_CONTENT, Notes.KEY_CONTENT);
		sNotesProjectionMap.put(Notes.KEY_CREATEAT, Notes.KEY_CREATEAT);
		sNotesProjectionMap.put(Notes.KEY_UPDATEAT, Notes.KEY_UPDATEAT);
	}

	/**
	 * This class helps open, create, and upgrade the database file.
	 */
	private static class DatabaseHelper extends SQLiteOpenHelper
	{

		DatabaseHelper(Context context)
		{
			super(context, Notes.DATABASE_NAME, null, Notes.DATABASE_VERSION);
		}

		@Override
		public void onCreate(SQLiteDatabase db)
		{
			db.execSQL("CREATE TABLE " + Notes.TABLE_NAME + " (" + Notes._ID + " INTEGER PRIMARY KEY," + Notes.KEY_TITLE + " TEXT," + Notes.KEY_CONTENT + " TEXT,"
					+ Notes.KEY_CREATEAT + " TEXT," + Notes.KEY_UPDATEAT + " TEXT" + ");");
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
		{
			Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data");
			db.execSQL("DROP TABLE IF EXISTS " + Notes.TABLE_NAME);
			onCreate(db);
		}
	}

	private DatabaseHelper mOpenHelper;

	@Override
	public boolean onCreate()
	{
		mOpenHelper = new DatabaseHelper(getContext());
		sqlitedb = mOpenHelper.getWritableDatabase();
		return true;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
	{
		SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

		switch (sUriMatcher.match(uri))
		{
		case NOTES:
			qb.setTables(Notes.TABLE_NAME);
			qb.setProjectionMap(sNotesProjectionMap);
			break;

		case NOTE_ID:
			qb.setTables(Notes.TABLE_NAME);
			qb.setProjectionMap(sNotesProjectionMap);
			qb.appendWhere(Notes._ID + "=" + uri.getPathSegments().get(1));
			break;

		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		// If no sort order is specified use the default
		String orderBy;
		if (TextUtils.isEmpty(sortOrder))
		{
			orderBy = Notes.DEFAULT_SORT_ORDER;
		}
		else
		{
			orderBy = sortOrder;
		}

		Cursor c = qb.query(sqlitedb, projection, selection, selectionArgs, null, null, orderBy);

		// Tell the cursor what uri to watch, so it knows when its source data changes
		c.setNotificationUri(getContext().getContentResolver(), uri);
		return c;
	}

	@Override
	public String getType(Uri uri)
	{
		Log.i(TAG, "getType: the uri is: " + uri);
		
		switch (sUriMatcher.match(uri))
		{
		case NOTES:
			return Notes.CONTENT_TYPE;

		case NOTE_ID:
			return Notes.CONTENT_ITEM_TYPE;

		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}
	}

	@Override
	public Uri insert(Uri uri, ContentValues initialValues)
	{
		// Validate the requested uri
		if (sUriMatcher.match(uri) != NOTES)
		{
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		ContentValues values;
		if (initialValues != null)
		{
			values = new ContentValues(initialValues);
		}
		else
		{
			values = new ContentValues();
		}

		// Make sure that the fields are all set
		if (values.containsKey(NotePad.Notes.KEY_CREATEAT) == false)
		{
			values.put(NotePad.Notes.KEY_CREATEAT, DateUtil.getToday());
		}

		if (values.containsKey(NotePad.Notes.KEY_UPDATEAT) == false)
		{
			values.put(NotePad.Notes.KEY_UPDATEAT, DateUtil.getTodayTimeStampString());
		}

		if (values.containsKey(NotePad.Notes.KEY_TITLE) == false)
		{
			Resources r = Resources.getSystem();
			values.put(NotePad.Notes.KEY_TITLE, r.getString(android.R.string.untitled));
		}

		if (values.containsKey(NotePad.Notes.KEY_CONTENT) == false)
		{
			values.put(NotePad.Notes.KEY_CONTENT, "");
		}

		long rowId = sqlitedb.insert(Notes.TABLE_NAME, Notes.KEY_CONTENT, values);
		if (rowId > 0)
		{
			Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_URI, rowId);
			getContext().getContentResolver().notifyChange(noteUri, null);
			return noteUri;
		}

		throw new SQLException("Failed to insert row into " + uri);
	}

	@Override
	public int delete(Uri uri, String where, String[] whereArgs)
	{
		int count;
		switch (sUriMatcher.match(uri))
		{
		case NOTES:
			count = sqlitedb.delete(Notes.TABLE_NAME, where, whereArgs);
			break;

		case NOTE_ID:
			String noteId = uri.getPathSegments().get(1);
			count = sqlitedb.delete(Notes.TABLE_NAME, Notes._ID + "=" + noteId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
			break;

		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		getContext().getContentResolver().notifyChange(uri, null);
		return count;
	}

	@Override
	public int update(Uri uri, ContentValues values, String where, String[] whereArgs)
	{
		int count;
		switch (sUriMatcher.match(uri))
		{
		case NOTES:
			count = sqlitedb.update(Notes.TABLE_NAME, values, where, whereArgs);
			break;

		case NOTE_ID:
			String noteId = uri.getPathSegments().get(1);
			count = sqlitedb.update(Notes.TABLE_NAME, values, Notes._ID + "=" + noteId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
			break;

		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		getContext().getContentResolver().notifyChange(uri, null);
		return count;
	}

	
}

 

 NotePad.java

package com.cticc.notepad;

import android.net.Uri;
import android.provider.BaseColumns;

/**
 * Convenience definitions for NotePadProvider
 */
public final class NotePad
{
	public static final String AUTHORITY = "com.google.provider.NotePad";

	// This class cannot be instantiated
	private NotePad()
	{

	}

	/**
	 * Notes table
	 */
	public static final class Notes implements BaseColumns
	{
		// This class cannot be instantiated
		private Notes()
		{
		}

		/****************************************************************************************
		 * 
		 * 
		 * **************************************************************************************/
		public static final String DATABASE_NAME = "notepad.db";
		public static final String TABLE_NAME = "notes";
		public static final int DATABASE_VERSION = 2;

		/****************************************************************************************
		 * 
		 * 
		 * **************************************************************************************/
		// public static final String KEY_ROWID = "_id";
		public static final String KEY_TITLE = "title";
		public static final String KEY_CONTENT = "content";
		public static final String KEY_CREATEAT = "createAt";
		public static final String KEY_UPDATEAT = "updateAt";

		/****************************************************************************************
		 * 
		 * 
		 * **************************************************************************************/
		/**
		 * The content:// style URL for this table
		 */
		public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);

		/**
		 * The MIME type of {@link #CONTENT_URI} providing a directory of notes.
		 */
		public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";

		/**
		 * The MIME type of a {@link #CONTENT_URI} sub-directory of a single note.
		 */
		public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.note";

		/**
		 * The default sort order for this table
		 */
		public static final String DEFAULT_SORT_ORDER = "updateAt DESC";

	}
}

 

DateUtil.java

 

package com.cticc.notepad.util;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil
{
	private static String defaultDatePattern = "yyyy-MM-dd";
	
	private static String defaultTimeStampPattern = "yyyy-MM-dd HH:mm:ss";

	/**
	 * 返回默认格式的当前日期
	 * 
	 * @return String
	 */
	public static String getToday()
	{
		Date today = new Date();
		return convertDateToString(today,defaultDatePattern);
	}
	
	/**
	 * 返回默认格式的当前时间戳字符串格式
	 * 
	 * @return String
	 */
	public static String getTodayTimeStampString()
	{
		Date today = new Date();
		return convertDateToString(today, defaultTimeStampPattern);
	}

	/**
	 * 使用默认格式转换Date成字符串
	 * 
	 * @param date
	 * @return String
	 */
	public static String convertDateToString(Date date)
	{
		return convertDateToString(date, defaultDatePattern);
	}

	/**
	 * 使用指定格式转换Date成字符串
	 * 
	 * @param date
	 * @param pattern
	 * @return String
	 */
	public static String convertDateToString(Date date, String pattern)
	{
		String returnValue = "";

		if (date != null)
		{
			SimpleDateFormat df = new SimpleDateFormat(pattern);
			returnValue = df.format(date);
		}
		return returnValue;
	}
}

 

note_editor.xml:







		
		

			



 

noteslist_item.xml








    

   


     
    

 

 strings.xml





    Confirm
    Delete
    Discard
    
    
    Add note

    Add note
    Edit note
	Title
	
	note_editor
	noteslist_item
	
	NotePad
	 | 
	

	Error
	Error loading note

 

 

 

 

AndroidManifest.xml






    
        

        
            
                
                
            
            
                
                
                
                
                
            
            
                
                
                
            
        
        
        
            
            
                
                
                
                
                
            

            
            
                
                
                
            

        
        
        
    


 

 

上几张图片:

 



 


 



 
 

 

 

在下面这个界面,我本来是想把edit换成delete,可是在onPrepareOptionsMenu()中换了之后菜单就出不来了。

郁闷~~~~

 

 



 

你可能感兴趣的:(Android)