`

2011.10.19——— android 显示一行内容并录制其音频

阅读更多
2011.10.19——— android 显示一行内容并录制其音频

一个小工具

同事作语音识别 需要其他人帮他录制音频,台式机没有麦克 所以希望用手机来录,就需要一个小程序

需求:
1、装载一个txt文本,每次读取一行
2、根据这个文本进行录音
3、将pcm文件与每行文字 的对应关系 写入一个txt文本当中

思路:

1、录制pcm
2、选着文本 一个简易的文件管理器

代码:

录制:

package com.lp.read;



import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
/**
 * AudioRecord录制PCM AudioTrack播放
 * @author Administrator
 *
 */
public class MainActivity2 extends Activity implements OnClickListener{
	
	
	private Button btnStart,btnCheck;
//	private Button btnPlay2;
	
	private RecordTask recorder;
	private PlayTask player;
	
	private String mFileName;
	private String mDirName;
    private String sdcard;
	
	private boolean isRecording=true, isPlaying=false, mStartRecording = true;; 
	
	private int frequence = 16000; 
	private int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
	private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
	
	private AudioManager am;
	
	private int mode;
	
	private TextView tv;
    
    private List<String> texts = new ArrayList<String>();
    private int index = 1;
    
    private boolean isWrite = true;
    private String writeFileName;
    private WriteThread thread;
	
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main_pcm);
		
		sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
        mDirName = sdcard +  "/read";
    	File f = new File(mDirName);
    	if(!f.exists()){
			f.mkdir();
    	}
		
		am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
		
		btnStart = (Button)this.findViewById(R.id.btn_start);
//		btnPlay2 = (Button)this.findViewById(R.id.btn_play2);
		btnStart.setOnClickListener(this);
//		btnPlay2.setOnClickListener(this);
		btnStart.setEnabled(false);
//		btnPlay2.setEnabled(false);
		
		btnCheck = (Button)findViewById(R.id.btn_check);
		btnCheck.setOnClickListener(this);
		
		tv = (TextView)findViewById(R.id.tv);
		
		thread = new WriteThread();
		thread.start();
	}
	
	
	public void onClick(View v){
		int id = v.getId();
		switch(id){
		case R.id.btn_start:
			if (mStartRecording) {
				this.isRecording = true;
				mFileName = mDirName + "/"+new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss").format( new Date().getTime())+".pcm";
    			recorder = new RecordTask();
    			recorder.execute();
    			
                ((Button)v).setText("录音停止");
            } else {
            	this.isRecording = false;
            	((Button)v).setText("录音开始");
            	changeText();
            	thread.setfName(mFileName);
            	thread.setText(tv.getText().toString());
//            	btnPlay2.setEnabled(true);
            }
            mStartRecording = !mStartRecording;
			
			break;
//		case R.id.btn_play2:
//			this.isPlaying = true;
//			btnPlay2.setEnabled(false);
//			mode = AudioManager.STREAM_MUSIC;
//			
//			if(!am.isSpeakerphoneOn()){
//				am.setSpeakerphoneOn(true);
//			}
//			setVolumeControlStream(AudioManager.STREAM_MUSIC);
//			am.setMode(AudioManager.MODE_NORMAL);
//			am.setStreamVolume(AudioManager.STREAM_MUSIC, am.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0); 
//			
//			player = new PlayTask();
//			player.execute();
//			break;
		case R.id.btn_check:
			
			Intent intent = new Intent(this,FileOSActivity.class);
			startActivityForResult(intent, 100);
			break;
		}
	}
	
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		if(data!=null ){
			String path = data.getStringExtra("path");
			String name = path.substring(path.lastIndexOf("/")+1).toLowerCase().replace(".txt", "");
			//清空数据
			texts.clear();
			index = 1;
			writeFileName = mDirName + "/"+name+"_"+new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss").format( new Date().getTime())+".txt";
			new ReadTextAsyncTask().execute(path);
			btnStart.setEnabled(true);
		}
	}


	private void changeText(){
    	System.out.println(texts.size());
    	if(index < texts.size()){
	    	tv.setText(texts.get(index));
	    	index++;
    	}else{
    		texts.clear();
    		index = 1;
    		btnStart.setEnabled(false);
    		tv.setText("");
    		Toast.makeText(this, "此文件已读完。", 1).show();
    	}
    }
	
	class RecordTask extends AsyncTask<Void, Integer, Void>{
		@Override
		protected Void doInBackground(Void... arg0) {
			AudioRecord record = null;
			DataOutputStream dos = null;
			//FileOutputStream fos = null;
			//isRecording = true;
			try {
				//开通输出流到指定的文件
				dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mFileName)));
				//fos = new FileOutputStream(audioFile);
				//根据定义好的几个配置,来获取合适的缓冲大小
				int bufferSize = AudioRecord.getMinBufferSize(frequence, channelConfig, audioEncoding);
				//实例化AudioRecord
				record = new AudioRecord(MediaRecorder.AudioSource.MIC, frequence, channelConfig, audioEncoding, bufferSize);
				//定义缓冲
				short[] buffer = new short[bufferSize];
				//byte[] buffer = new byte[bufferSize];
				
				//开始录制
				record.startRecording();
				
				//定义循环,根据isRecording的值来判断是否继续录制
				while(isRecording){
					//从bufferSize中读取字节,返回读取的short个数
					int bufferReadResult = record.read(buffer, 0, buffer.length);
					//循环将buffer中的音频数据写入到OutputStream中
					for(int i=0; i<bufferReadResult; i++){
						dos.writeShort(buffer[i]);
					}
					
//					byte[] tmpBuf = new byte[bufferReadResult];  
//                    System.arraycopy(buffer, 0, tmpBuf, 0, bufferReadResult); 
//                    fos.write(tmpBuf, 0, bufferReadResult);
//                    fos.flush();
				}
				//录制结束
				record.stop();
				
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				record.release();
				try {
					dos.close();
					//fos.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			}
			return null;
		}
	}
	
	class PlayTask extends AsyncTask<Void, Integer, Void>{
		@Override
		protected Void doInBackground(Void... arg0) {
			AudioTrack track = null;
			DataInputStream dis = null;
			int bufferSize = AudioTrack.getMinBufferSize(frequence, channelConfig, audioEncoding);
			short[] buffer = new short[bufferSize];
			//byte[] buffer = new byte[bufferSize];
			try {
				//定义输入流,将音频写入到AudioTrack类中,实现播放
				dis = new DataInputStream(new BufferedInputStream(new FileInputStream(mFileName)));
				//FileInputStream fis = new FileInputStream(audioFile);
				//实例AudioTrack
				//AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, frequence, channelConfig, audioEncoding, bufferSize, AudioTrack.MODE_STREAM);
				track = new AudioTrack(mode, frequence, AudioFormat.CHANNEL_OUT_MONO, audioEncoding, bufferSize, AudioTrack.MODE_STREAM);
				//开始播放
				track.play();
				//由于AudioTrack播放的是流,所以,我们需要一边播放一边读取
				while(isPlaying && dis.available()>0){
					int i = 0;
					while(dis.available()>0 && i<buffer.length){
						buffer[i] = dis.readShort();
						i++;
					}
					//然后将数据写入到AudioTrack中
					track.write(buffer, 0, buffer.length);
					
				}
				
				//播放结束
				track.stop();
				
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				track.release();
				try {
					dis.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			return null;
		}
		
		@Override
		protected void onPostExecute(Void result) {
//			btnPlay2.setEnabled(true);
		}
		
	}
	
	
	private class ReadTextAsyncTask extends AsyncTask<String, Void, Void>{

		@Override
		protected Void doInBackground(String... params) {
			String path = params[0];
			BufferedReader br = null;
			try {
				br = new BufferedReader(new InputStreamReader(new FileInputStream(path),"GB2312"));
				String readStr = null;
				while((readStr = br.readLine()) != null){
					texts.add(readStr);
					
					//第一次的时候  改变一下textview
					if(texts.size()==1){
						publishProgress();
					}
				}
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				try{
					br.close();
				}catch(Exception e){
					e.printStackTrace();
				}
			}
			return null;
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
		}

		@Override
		protected void onProgressUpdate(Void... values) {
			super.onProgressUpdate(values);
			tv.setText(texts.get(0));
		}
    }
	
	private class WriteThread extends Thread{

		String fName = null;
		String text = null;
		
		@Override
		public void run() {
			
			while(isWrite){
				
				if(fName!=null && text!=null && !fName.equals("") && !text.equals("")){
					write();
					fName = null;
					text = null;
				}
			}
			
		}
		
		private void write(){
			BufferedWriter bw = null;
			try {
				bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(writeFileName,true)));
				bw.write(text+"---------------->>"+fName);
				bw.write("\r\n");
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				try{
					bw.close();
				}catch(Exception e){
					e.printStackTrace();
				}
			}
		}
		
		public void setfName(String fName){
			this.fName = fName;
		}
		
		public void setText(String text){
			this.text = text;
		}
		
	}

	@Override
	protected void onPause() {
		super.onPause();
//		isWrite = false;
//		isRecording = false;
	}
	
	
	
}



文件管理器:

package com.lp.read;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

public class FileOSActivity extends ListActivity {

	static final int REQUEST_CODE = 1;

	private static String SDpath;

	// 显示模式
	private enum DISPLAYMODE {
		ABSOLUTE, RELATIVE;
	}

	private final DISPLAYMODE displayMode = DISPLAYMODE.RELATIVE;

	private List<IconifiedText> directoryEntries = new ArrayList<IconifiedText>();

	public File currentDirectory = new File("/");// 当前目录

	public boolean TCardExist;

	// private File currentDirectory = new File(filePath);

	/** Called when the activity is first created. */
	/** Activity被创建时调用 */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		TCardExist = judgeSDcard();
		if (TCardExist) {
			SDpath = getSDPath();
			Log.e("yangw getSDPath", SDpath);

			EnterFolder(new File(SDpath));
			// browseTo(new File("/"));
			this.setSelection(0);
		} else {
			Log.e("yangw", "SDcard is not exist");
			setContentView(R.layout.nosdcard);
		}
		// this.setSelection(0);
	}

	// 判断sd卡是否存在
	public boolean judgeSDcard() {

		boolean sdCardExist = Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED);
		return sdCardExist;
	}

	// 获取sd卡路径 Environment
	public String getSDPath() {
		File sdDir = null;
		sdDir = Environment.getExternalStorageDirectory();// 获取跟目录

		return sdDir.toString();
	}

	/**
	 * This function browses up one level according to the field: currentDirectory
	 */
	private void upOneLevel() {
		if (this.currentDirectory.getParent() != null)
			this.EnterFolder(this.currentDirectory.getParentFile());
	}

	private void EnterFolder(final File aDirectory) {
		if (this.displayMode == DISPLAYMODE.RELATIVE)

			// getAbsolutePath 得到一个文件的绝对路径
			this.setTitle(aDirectory.getAbsolutePath());
		Log.e("open folder aDirectory.getAbsolutePath()", aDirectory.getAbsolutePath());

		this.currentDirectory = aDirectory;
		Log.e("Fill 路径", aDirectory.getPath());
		FillList(aDirectory.listFiles());
	}

	private void openFile(File f) {
		Log.e("yangw mark", f.getAbsolutePath());
		String fName = f.getName();
		String end = fName.substring(fName.lastIndexOf(".") + 1, fName.length()).toLowerCase();
		if(end.equals("txt")){
			Intent intent = new Intent();
			intent.putExtra("path", f.getAbsolutePath());
			setResult(100, intent);
			finish();
		}else{
			Toast.makeText(this, "请选择txt文本结构!", 0).show();
		}

	}

	private void FillList(File[] files) {

		this.directoryEntries.clear();// 清空列表

		String momentpath = GetCurDirectory();
		Log.e("GetCurDirectory momentpath", momentpath);
		Log.e("getSDPath sdpath", SDpath);
		Log.e("this.currentDirectory.getParent()", this.currentDirectory.getParent());
		// and the ".." == 'Up one level'
		
		for (File currentFile : files) {
			// 显示模式?
			switch (this.displayMode) {
				case ABSOLUTE:

					/* On absolute Mode, we show the full path */
					this.directoryEntries.add(new IconifiedText(currentFile.getAbsolutePath(), null));

					break;
				case RELATIVE:
					/*
					 * On relative Mode, we have to cut the current-path at the beginning
					 */
					int currentPathStringLenght = this.currentDirectory.getAbsolutePath().length();

					if (this.currentDirectory.getParent() != null) {
						this.directoryEntries.add(new IconifiedText(currentFile.getAbsolutePath().substring(currentPathStringLenght + 1), null));
					} else {
						this.directoryEntries.add(new IconifiedText(currentFile.getAbsolutePath().substring(currentPathStringLenght), null));
					}

					break;
			}
		}

		Collections.sort(this.directoryEntries);// 排序
		
		// 如果不是根目录则添加上一级目录
		if (this.currentDirectory.getParent() != null && momentpath != SDpath)
			if (!momentpath.equals(SDpath)) {
				this.directoryEntries.add(0,new IconifiedText(getString(R.string.up_one_level), null));
			}

		IconifiedTextListAdapter itla = new IconifiedTextListAdapter(this);
		itla.setListItems(this.directoryEntries);
		this.setListAdapter(itla);
	}

	// mark
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		Log.e("yangw", "short click listview: onListItemClick");
		super.onListItemClick(l, v, position, id);

		String selectedFileString = this.directoryEntries.get(position).getText();

		Log.e("onListItemClick Item named", selectedFileString);

		if (selectedFileString.equals(getString(R.string.current_dir))) {
			// Refresh 刷新
			Log.e("选择Items", "刷新");
			this.EnterFolder(this.currentDirectory);
		} else if (selectedFileString.equals(getString(R.string.up_one_level))) {
			// 到上一阶目录
			Log.e("选择Items", "返回上级");
			this.upOneLevel();
		} else {

			File clickedFile = null;
			switch (this.displayMode) {
				case RELATIVE:
					clickedFile = new File(this.currentDirectory.getAbsolutePath() + "/" + this.directoryEntries.get(position).getText());
					break;
				case ABSOLUTE:
					clickedFile = new File(this.directoryEntries.get(position).getText());
					break;
			}
			if (clickedFile.isDirectory()) {
				Log.e("打开文件夹", clickedFile.getName());
				this.EnterFolder(clickedFile);
			} else {
				Log.e("打开文件", clickedFile.getName());
				openFile(clickedFile);
			}

		}
	}

	// 得到当前目录的绝对路径
	public String GetCurDirectory() {
		return this.currentDirectory.getAbsolutePath();
	}

}



效果如图所示:





代码见附件



  • 大小: 122.6 KB
分享到:
评论

相关推荐

    基于Selenium的Java爬虫实战(内含谷歌浏览器Chrom和Chromedriver版本116.0.5808.0)

    资源包括: 1.Java爬虫实战代码 2.selenium学习笔记 3.代码演示视频 4.谷歌浏览器chrom116.0.5808.0 chrome-linux64.zip chrome-mac-arm64.zip chrome-mac-x64.zip chrome-win32.zip chrome-win64.zip 5.谷歌浏览器驱动器Chromedriver116.0.5808.0 chromedriver-linux64.zip chromedriver-mac-arm64.zip chromedriver-mac-x64.zip chromedriver-win32.zip chromedriver-win64.zip 特别说明:Chrome 为测试版(不会自动更新) 仅适用于自动测试。若要进行常规浏览,请使用可自动更新的标准版 Chrome。)

    2024消费趋势报告.pdf

    2024消费趋势报告.pdf

    PCB的电磁兼容设计+电子设计领域

    1、EMC由EMI和EMS组成 2、EMS常见的整改措施 3、干扰=共模干扰+差模干扰 4、元器件的摆放 5、缝隙影响

    给排水施工图全套.xdw.pdf

    给排水施工图全套.xdw

    基于微信小程序的旅游出行必备(后端接口ssm框架实现)-毕设项目

    毕业设计基于微信小程序的旅游出行必备(后端接口ssm框架实现)-毕设项目.zip 个人经导师指导并认可通过的高分设计项目,评审分98分。主要针对计算机相关专业的正在做毕设的学生和需要项目实战练习的学习者,也可作为课程设计、期末大作业。 项目介绍: 旅游出行必备项目, 前端为微信小程序,后端接口为ssm框架实现,项目包含源码、数据库毕业设计基于微信小程序的旅游出行必备(后端接口ssm框架实现)-毕设项目.zip 个人经导师指导并认可通过的高分设计项目,评审分98分。主要针对计算机相关专业的正在做毕设的学生和需要项目实战练习的学习者,也可作为课程设计、期末大作业。 项目主要功能: 该项目是一个基于微信小程序的旅游出行应用,结合后端SSM(Spring MVC + Spring + MyBatis)框架提供服务。主要功能包括行程规划、景点信息查询、在线预订等,旨在为用户提供便捷的旅游出行体验。特点在于利用微信小程序的便捷性,实现即用即走,同时通过后端强大的数据处理能力保证服务稳定性。技术栈涵盖微信小程序开发、Java SSM框架、数据库管理等,适合学习和作为毕业设计参考。

    MC/00000000000000000000000000

    Want to play MC but do not want to buy can download to have a look。 It's very interested ------------------------------------------------------------------------------------------------- ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo

    2024嵌入式面试资料SPI子系统

    2024嵌入式面试资料SPI子系统提取方式是百度网盘分享地址

    电子迎宾器和声光控照明开关multsim仿真和课设报告

    设计报告 ——题目名称:电子迎宾器 ——题目名称: 声光控照明开关

    基于Selenium的Java爬虫实战(内含谷歌浏览器Chrom和Chromedriver版本115.0.5789.0)

    资源包括: 1.Java爬虫实战代码 2.selenium学习笔记 3.代码演示视频 4.谷歌浏览器chrom115.0.5789.0 chrome-linux64.zip chrome-mac-arm64.zip chrome-mac-x64.zip chrome-win32.zip chrome-win64.zip 5.谷歌浏览器驱动器Chromedriver115.0.5789.0 chromedriver-linux64.zip chromedriver-mac-arm64.zip chromedriver-mac-x64.zip chromedriver-win32.zip chromedriver-win64.zip 特别说明:Chrome 为测试版(不会自动更新) 仅适用于自动测试。若要进行常规浏览,请使用可自动更新的标准版 Chrome。)

    基于Selenium的Java爬虫实战(内含谷歌浏览器Chrom和Chromedriver版本115.0.5785.0)

    资源包括: 1.Java爬虫实战代码 2.selenium学习笔记 3.代码演示视频 4.谷歌浏览器chrom115.0.5785.0 chrome-linux64.zip chrome-mac-arm64.zip chrome-mac-x64.zip chrome-win32.zip chrome-win64.zip 5.谷歌浏览器驱动器Chromedriver115.0.5785.0 chromedriver-linux64.zip chromedriver-mac-arm64.zip chromedriver-mac-x64.zip chromedriver-win32.zip chromedriver-win64.zip 特别说明:Chrome 为测试版(不会自动更新) 仅适用于自动测试。若要进行常规浏览,请使用可自动更新的标准版 Chrome。)

    navicat下载、安装、配置.zip

    【navicat】下载、安装、配置

    2024嵌入式大厂面经东华软件2015年应届实习生毕业生招聘职位-公司

    2024嵌入式大厂面经东华软件2015年应届实习生毕业生招聘职位-公司提取方式是百度网盘分享地址

    C++ 高性能爬虫代码,带UI

    C++ 高性能爬虫代码,带UI

    2024年铁基非晶合金行业分析报告.pptx

    行业报告

    2024嵌入式大厂面经朝歌宽带笔试题

    2024嵌入式大厂面经朝歌宽带笔试题提取方式是百度网盘分享地址

    AutoRuns 查看设置开机自动加载程序、病毒木马及恶意插件程序等

    Autoruns for Windows 是 Mark Russinovich 和 Bryce Cogswell 开发的一款软件,它能用于显示在 Windows启动或登录时自动运行的程序,并且允许用户有选择地禁用或删除它们,例如那些在“启动”文件夹和注册表相关键中的程序。此外,Autoruns还可以修改包括:Windows 资源管理器的 Shell 扩展(如右键弹出菜单)、IE浏览器插件(如工具栏扩展)、系统服务和设备驱动程序、计划任务等多种不同的自启动程序。 Autoruns作为Sysinternals Suite(故障诊断工具套装)的一部分,可运行于 Windows XP、Windows Server 2003 和更高版本的 Windows 操作系统。该软件还包括一个相同功能的命令行版本Autorunsc,可以把结果报表以 CSV 格式输出。

    2024年市场展望与薪酬报告发布-randstad.pdf

    2024年市场展望与薪酬报告发布-randstad.pdf

    AIGC 之 AI 绘画:技术与应用双突破,生产力变革在即

    AIGC 之 AI 绘画:技术与应用双突破,生产力变革在即

    智能车竞赛介绍.docx

    智能车竞赛介绍.docx

    Android开发基本知识总结,Android课件pdf

    1.什么是Android? 移动设备(常常指手机mobile)的操作系统,由google公司出品,是免费的,最早是基于Linux平台的:目前已经推出的Android3.0是平板电脑上的操作系统。 其它的手机操作系统有:iPhone系统等。 手机厂商三星(Samsung,宏达电(HTC),摩托罗拉(Motorola),华硕,索尼,Garmin等都支持iPhone系统。 Android系统功能自定义用户界面,支持蓝牙,多点触摸,具有导航功能和语音识别功能结合了google 地图。Android 软件开发工具SDK4.0 Android系统特点:虚拟键盘,以WebKit为核心的WebView组件,可以使用HTML,javascript 等。

Global site tag (gtag.js) - Google Analytics