AutoRing Android 服务创建演练
创建一个监听连接更改的简单服务。
引言
本演练将涵盖服务应用程序的创建以及以下主题
- 服务创建
- 使用通知
- 上下文菜单
- 电池状态检查
- 持久化用户设置
- 创建图标
该项目是使用 Eclipse 和 Android SDK 构建的。
背景
我创建此应用程序是为了模拟 Motorola RAZR 的响铃行为,该行为会在充电时切换到响铃模式,在断开连接时恢复到仅振动模式(无需手动调整)。此应用程序还允许反向场景(连接时静音,否则响铃)。
本教程假设您已经设置并运行了 Eclipse 环境。如果您是 Eclipse 和 Android 开发的新手,我建议您阅读温度转换器教程,该教程可以在 这里 找到。
Using the Code
您可以通过遵循以下步骤来创建项目。如果您希望加载整个项目,请下载\解压项目文件,然后打开 Eclipse 并选择 File->Import..->General->Existing Projects,然后选择 AutoRing
项目的根文件夹。
开始吧
启动 Eclipse(我使用的是 Eclipse Classic 版本 3.6.2)。
选择 File -> New -> Project -> Android -> Android Project
单击“下一步”。
按如下方式填写字段。您可以使用 Android 1.5 或更高版本的任何版本。
单击“完成”。
项目创建后,将这些图标添加到 AutoRing\res\drawable 文件夹。您可以直接将它们拖到 Eclipse 中的文件夹,也可以使用 Windows Explorer。如果使用 Explorer,请右键单击 Eclipse 中的文件夹并选择 Refresh 以查看新文件。请务必按照以下列表命名文件,因为我们的项目将引用这些名称。我们将在本演练结束时讨论如何创建自己的图标。
circlefill.png
circleopen.png
downarrow.png
mainicon.png
添加图标后,展开的 res\drawable 文件夹应如下所示
打开 AndroidManifest.xml。
单击源编辑器中的最后一个选项卡以查看实际 XML。
如果您使用的是 Android 1.6 或更高版本,请将 sdk 版本更新为 4。在版本 3 中,安装此应用程序将需要电话和存储权限。Android 1.5 需要 sdk 版本 3(否则应用程序将无法启动)。此值也可以在“New Android Project”对话框中设置,但无论哪种方式都可以。
<uses-sdk android:minSdkVersion="4" />
更新 application 标签以设置应用程序图标和标题。
<application android:icon="@drawable/mainicon" android:label="AutoRing">
向 activity
标签添加 launchMode
属性。将其设置为 singleInstance
。这将防止我们的 activity 运行多个实例。如果没有此设置,用户可以从主屏幕启动我们的应用程序,然后从通知屏幕启动另一个实例(实际上是几个)。
<activity android:name=".AutoRingActivity"
android:label="@string/app_name"
android:launchMode="singleInstance">
为我们将要创建的服务添加 service 标签。将 exported 设置为 false
可确保没有其他应用程序可以与我们的服务通信。还添加了 receiver 标签,该标签与我们的 RebootReceiver
类一起用于在手机重新启动时重新启动我们的服务。如果您希望手动重新启动服务,则可以省略这部分。
.........
</activity>
<service android:name=".AutoRingSvc" android:exported="false" />
<receiver android:name="RebootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
........
打开 main.xml 并单击第二个选项卡以查看实际 XML。
删除 main.xml 中的现有 XML。
为我们的主 activity 添加标签。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/main_view"
android:gravity="center_horizontal"
xmlns:android="http://schemas.android.com/apk/res/android">
为 2 个状态文本框添加标签。
<TextView
android:id="@+id/txtStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="AutoRing is stopped"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<TextView
android:id="@+id/txtBattStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"/>
为选项按钮添加标签。marginTop
属性将在按钮上方留出一些空间。
<Button
android:layout_marginTop="25sp"
android:id="@+id/btnConnectedOption"
android:text="When Connected:"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:gravity="center_vertical|left"/>
<Button
android:id="@+id/btnDisconnectedOption"
android:text="When Disconnected:"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:gravity="center_vertical|left"/>
最后,为服务 Start\Stop 按钮添加标签。
<Button
android:layout_marginTop="40sp"
android:id="@+id/btnStartStop"
android:text="Start AutoRing"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"/>
</LinearLayout>
此布局在我们的应用程序运行时应创建以下 GUI(箭头在代码中添加)
接下来,我们将添加服务类。
右键单击 AutoRing
项目并选择 New->Class。
按如下方式输入 Name、Package 和 Superclass
单击“完成”。
我们还需要一个类。这将允许我们的服务在手机重新启动时自动重新启动。如果您希望在手机重启后手动重新启动服务,则可以跳过此步骤和下一个编码步骤。
编码 RebootReceiver
我们需要向 RebootReceiver
类添加几行代码来重新启动我们的服务。我们读取 StartOnReboot
首选项(在 AutoRingActivity.onClick
中设置)来确定是否应启动服务。可能有多项服务尝试在启动时启动,因此我们将等待 30 秒后再启动我们的服务。我们将使用 Timer 来延迟服务启动。我们再次检查首选项(尽管不太可能)以防万一用户在 30 秒延迟内启动/停止了服务。按如下方式更新 onReceive
方法
@Override //this gets called after a phone reboot
public void onReceive(Context context, Intent intent) {
//component context not enough, get app context for preferences
final Context ctxt = context.getApplicationContext();
//conditional service restart, only restart if it was running before reboot
if (!ctxt.getSharedPreferences("AutoRing", 0).getBoolean("StartOnReboot", false))
return; //don't start service
//use timer to delay service start so phone remains responsive at boot
(new java.util.Timer()).schedule(
new java.util.TimerTask() {
@Override
public void run() //restart the service
{ //recheck in case user started and
//stopped service within 30 seconds
if (ctxt.getSharedPreferences
("AutoRing", 0).getBoolean("StartOnReboot", false))
ctxt.startService(
new Intent(ctxt, AutoRingSvc.class));
}
}, 30000); //wait 30 seconds
} //onReceive
getApplicationContext()
是获取我们应用程序全局上下文所必需的。传递到onReceive
的上下文是组件上下文,其中不包含正确的首选项。sleep()
方法在此处不起作用。我认为进程在睡眠时会被转储。- 将
BOOT_COMPLETED
操作添加到 AndroidManifest.xml 将在启动时触发RebootReceiver
,即使应用程序/服务从未运行过(仅安装)。
编码 AutoRingActivity 类
打开 AutoRingActivity.java。
这是允许用户为我们的服务类设置选项以及启动和停止服务的类。请注意,即使服务类尚未完成,我们也将对其进行一些引用。在编写服务之前,这会产生一些编译错误。
删除 AutoRingActivity.java 中的所有现有代码。
添加应用程序所需的包名和导入。
package droid.ar;
import droid.ar.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View.OnCreateContextMenuListener;
import android.widget.TextView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import android.content.Intent;
import android.view.View;
创建 AutoRingActivity
类。我们的类将实现 2 个点击监听器接口,以便我们可以使用我们的 activity 来处理菜单和按钮点击(而不是创建单独的监听器类)。
public class AutoRingActivity extends Activity
//implement click listeners so this class can process menu & button clicks
implements MenuItem.OnMenuItemClickListener, View.OnClickListener {
添加我们的 activity 指针和按钮对象的类级别变量。
//class level variables
AutoRingActivity mMainActivity;
LinearLayout mMainView;
Button btnConnectedOption, btnDisconnectedOption, btnStartStop;
private int mConnectAction = 2; //default is Ring
private int mDisconnectAction = 1; //default is Vibrate
开始 onCreate
处理程序。执行一些准备工作,然后创建按钮对象并设置它们的监听器。对于 Android 1.5,必须在代码中设置监听器。1.5 之后,可以在 main.xml 中声明监听器。请注意,我们将我们的 activity 实例分配给了 static
AutoRingSvc.mMainActivity
。我们尚未编写服务代码,因此 Eclipse 将给出编译器错误。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//housekeeping
setContentView(R.layout.main);
mMainView = (android.widget.LinearLayout) findViewById(R.id.main_view);
AutoRingSvc.mMainActivity = this; //so service can update GUI
mMainActivity = this; //for use in anonymous classes
//get button objects
btnConnectedOption = ((Button) findViewById(R.id.btnConnectedOption));
btnDisconnectedOption = ((Button) findViewById(R.id.btnDisconnectedOption));
btnStartStop = ((Button) findViewById(R.id.btnStartStop));
//for Android 1.5 and less, we need to add the click listeners in code
//after Android 1.5, these can be declared in main.xml
//(android:onClick="onClick")
btnConnectedOption.setOnClickListener(this);
btnDisconnectedOption.setOnClickListener(this);
btnStartStop.setOnClickListener(this);
添加代码以加载用户首选项。如果未设置首选项,变量将保持不变。0
(Context.MODE_PRIVATE
)表示我们希望将这些首选项保留为 AutoRing
应用程序的 private
。
//get user preferences - use defaults if not set
mConnectAction = getSharedPreferences("AutoRing", 0)
.getInt("ConnectAction", mConnectAction);
mDisconnectAction = getSharedPreferences("AutoRing", 0)
.getInt("DisconnectAction", mDisconnectAction);
添加对 GUI 更新方法的调用。
UpdateSvcStatus(); //update text boxes and buttons
添加上下文菜单的处理程序。当用户单击选项按钮时,这将显示选项菜单。我们将调用按钮实例作为标签传递给 view 参数。这将使我们能够确定菜单的正确标题。每个菜单项都分配了按钮 ID 作为 groupid,因此我们在处理菜单项点击时将知道调用按钮。选中的项由当前操作值确定。服务有一个 string static string
数组(mActionList
),其中列出了选项。我们使用它来填充上下文菜单。
//this is called when we pop up the context menu (openContextMenu)
mMainView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(
ContextMenu menu, View v, ContextMenuInfo menuInfo) {
//we're using the same context menu for the
//Connect and Disconnect buttons
Button btnCaller = (Button)v.getTag(); //we passed the
//calling button in the view tag
//set menu heading according to calling button
menu.setHeaderTitle(btnCaller == btnConnectedOption ?
"When Connected" : "When Disconnected");
//get action based on calling button and current setting
int action = (v.getTag() == btnConnectedOption) ?
mConnectAction : mDisconnectAction;
//create the menu items according the mActionList string array
//set the group id to the calling button id for later reference
//all the menu items have the same listener
for (int i=0; i<4; i++)
menu.add(
btnCaller.getId(),i,0, AutoRingSvc.mActionList[i])
//set checked if this item matches current setting
.setChecked(action == i)
//we implemented OnMenuItemClickListener
.setOnMenuItemClickListener(mMainActivity);
//set the menu items to checkable and exclusive
// so they appear as radio buttons
menu.setGroupCheckable(btnCaller.getId(), true, true);
} //onCreateContextMenu
});
} //onCreate
当我们的应用程序运行时,此代码应生成以下上下文菜单。
添加上下文菜单选择的处理程序。请记住,我们将上下文菜单项的 GroupId
设置为调用按钮的 ID。我们使用它来确定更新哪个选项。然后,我们根据选中的菜单项(item id)设置选项。然后,我们更新首选项集合和 GUI 字段。
//this is called when the user clicks a context menu item
//this is the implementation of OnMenuItemClickListener
public boolean onMenuItemClick(MenuItem item) {
//the calling button instance was passed to the context menu
//the groupid of the menu item is the calling button id
//update service option value with new setting
if (item.getGroupId() == R.id.btnConnectedOption)
mConnectAction = item.getItemId();
if (item.getGroupId() == R.id.btnDisconnectedOption)
mDisconnectAction = item.getItemId();
//store updated settings, these are read in AutoRingSvc.onCreate()
getSharedPreferences("AutoRing", 0).edit()
.putInt("ConnectAction", mConnectAction)
.putInt("DisconnectAction", mDisconnectAction)
.commit();
UpdateSvcStatus(); //update GUI
return true;
} //onMenuItemClick
添加按钮点击处理程序。Activity 屏幕上有 2 个选项按钮和一个 Start\Stop 按钮。如果服务未运行,则启动按钮将启动服务,否则将停止服务。对于选项按钮,我们将按钮实例附加到主视图实例(我们也可以使用类变量),然后手动启动上下文菜单。我们也可以将上下文菜单附加到 GUI 对象(registerForContextMenu
),但这需要长按才能激活菜单。我们还设置了 StartOnReboot
首选项,该选项在 RebootReceiver
类中读取。如果用户在此处手动停止了服务,我们则不希望在启动时启动该服务。
//this is called when the user clicks a button on our GUI
//OnClickListener implementation
public void onClick(View view) {
Button btn = (Button)view;
if (btn.getId()== R.id.btnStartStop)
{
AutoRingSvc.mMainActivity = this; //so service can update GUI
//store service state, this is read in RebootReceiver class
//if user started service, we will need to restart on reboot
getSharedPreferences("AutoRing", 0).edit()
.putBoolean("StartOnReboot", !AutoRingSvc.mSvcStarted).commit();
//create service intent (or connect to running service)
Intent svc = new Intent(mMainActivity, AutoRingSvc.class);
if (!AutoRingSvc.mSvcStarted) //if not started
getApplicationContext().startService(svc); //start service
else //already started, so attempt stop
//stopService returns true if stopped successfully
AutoRingSvc.mSvcStarted =
!getApplicationContext().stopService(svc);
UpdateSvcStatus(); //update GUI
}
//if user clicked on an option button
if (btn.getId()== R.id.btnConnectedOption ||
btn.getId()== R.id.btnDisconnectedOption)
{
mMainView.setTag(btn); //pass calling button to context menu
//so we get button id back
openContextMenu(mMainView); //start option menu
}
} //onClick
添加更新 GUI 对象的方法。有两个文本字段用于显示服务和连接状态。如果服务正在运行,则禁用选项按钮。将选项按钮文本设置为选定的选项值。对于 Android 1.6 及更高版本,我们将添加箭头图标到按钮,以便用户知道它们是选项按钮。似乎 Android 1.5 及更早版本不支持按钮图标。当按钮禁用时,我们会删除图标,因为图标不会像按钮那样变成浅灰色,这看起来很奇怪。
public void UpdateSvcStatus() {
//update the GUI text fields with the battery and service status
((TextView) findViewById(R.id.txtBattStatus)).setText(AutoRingSvc.mBatteryStatus);
((TextView) findViewById(R.id.txtStatus)).setText
("AutoRing is "+(AutoRingSvc.mSvcStarted? "running": "stopped"));
//set text of Start\Stop button
btnStartStop.setText((AutoRingSvc.mSvcStarted? "Stop": "Start") + " AutoRing");
//if service is running disable the option buttons, else enable them
btnConnectedOption.setEnabled(!AutoRingSvc.mSvcStarted);
btnDisconnectedOption.setEnabled(!AutoRingSvc.mSvcStarted);
//update option button text with current settings
btnConnectedOption.setText("When Connected: " +
AutoRingSvc.mActionList[mConnectAction]);
btnDisconnectedOption.setText("When Disconnected: " +
AutoRingSvc.mActionList[mDisconnectAction]);
//must be Android 1.6 or higher to add arrow icon to buttons
if (Integer.parseInt(android.os.Build.VERSION.SDK) > 3)
{
//add arrow icons to option buttons when enabled
btnConnectedOption.setCompoundDrawablesWithIntrinsicBounds
(0,0, btnConnectedOption.isEnabled()? R.drawable.downarrow: 0, 0);
btnDisconnectedOption.setCompoundDrawablesWithIntrinsicBounds
(0,0, btnDisconnectedOption.isEnabled()? R.drawable.downarrow: 0, 0);
}
} //UpdateSvcStatus
在 activity 类中添加以下方法以完成我们的工作。onPause
在我们的 activity 移至后台时被调用(用户启动了另一个应用程序),因此我们可以断开与服务的连接。onResume
在应用程序移至前台时被调用,因此可以重新连接到服务以获取更新。onDestroy
在我们的应用程序停止时被调用。Toast
调用只是为了证明 GUI 已结束但我们的服务仍在运行。您应该为生产应用程序删除此项。
@Override
public void onPause() { //app moved to background, no need for updates
super.onPause();
AutoRingSvc.mMainActivity = null; //disconnect from service
}
@Override
public void onResume() { //app moved to foreground (also occurs at app startup)
super.onResume();
AutoRingSvc.mMainActivity = this; //reconnect to service
UpdateSvcStatus(); //update GUI
}
@Override
public void onDestroy() {
super.onDestroy();
//show message so we know when the GUI app is gone and
//the service is still running.
Toast.makeText( mMainView.getContext(), " Goodbye GUI ",
Toast.LENGTH_SHORT).show();
}
} //AutoRingActivity
编码服务类
打开 AutoRingSvc.java 并删除现有代码。
添加服务所需的包名和导入。
package droid.ar;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.BatteryManager;
import android.os.IBinder;
import android.widget.Toast;
添加将用于在服务和 activity 类之间共享数据的 static
变量。还添加了默认通知音量和状态指示器。两个操作变量都设置为默认值。您可以将这些值更改为您偏好的设置。
public class AutoRingSvc extends Service {
//we use static variables to share data between the service and GUI app
//we could use binding instead, but this is simpler
public static AutoRingActivity mMainActivity;
public static String mBatteryStatus = "";
public static boolean mSvcStarted = false;
private int mConnectAction = 2; //default is Ring
private int mDisconnectAction = 1; //default is Vibrate
private int mNotificationVolume = 5; //Default volume
private int mLastStatus = -1;
添加 static
操作列表数组。这将用于填充选项菜单,并用作响铃器状态文本。响铃模式的 android 常量为 0-2,这允许我们在设置响铃模式时使用我们的数组索引而不是常量。
//possible actions when connected\disconnected
//we sorted them according to the RINGER values just for shorter code
public static String[] mActionList = {
"Silent", //AudioManager.RINGER_MODE_SILENT = 0
"Vibrate Only", //AudioManager.RINGER_MODE_VIBRATE = 1
"Ring", //AudioManager.RINGER_MODE_NORMAL = 2
"No Change"}; //No constant for this
添加必需的 onBind
处理程序。这是另一种(更复杂)与正在运行的服务通信的方式。我们将停止我们的服务以进行设置更改,因此不需要 onBind
。只需忽略此事件。
@Override //required for Service class
public IBinder onBind(Intent arg0) { return null; }
添加 onCreate
处理程序。每次服务启动时都会调用此方法。如果操作系统内存不足,它可能会停止我们的服务。当内存被释放时,我们的服务将自动重新启动,并且会再次调用 onCreate
。
我们通过 registerReceiver
调用告诉 Android 我们希望接收电池状态更新。
SetForeground
用于告诉 Android 我们希望保持服务运行,即使内存不足。SetForeground
仅在 Android 2.0 及更低版本中有效。从 2.1 开始,我们将需要使用 startForeground
来启动服务。
用户首选项取自首选项集合(在 AutoRingActivity.onMenuItemClick
中设置)。
我们还在此处记录服务启动。如果您使用手机(而不是虚拟设备)进行调试,您需要从 Android 市场获取(免费)LogCat 查看器。除非使用虚拟设备,否则 Log 消息不会显示在 Eclipse 调试器中。
@Override //Called on service start (by GUI or OS)
public void onCreate() {
super.onCreate();
android.util.Log.d("AutoRing","Service Starting");
//popup a message from the service. This will appear even if no GUI.
Toast.makeText(getApplicationContext(),
" Starting ", Toast.LENGTH_SHORT).show();
//listen for connect\disconnect
registerReceiver( batteryReceiver,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED) );
//retrieve stored settings that were set in GUI
//SharedPreferences object is not shared across
//processes so we can use any name
//keep default values if preference not set (should never happen)
mConnectAction = getSharedPreferences("AutoRing", 0)
.getInt("ConnectAction", mConnectAction);
mDisconnectAction = getSharedPreferences("AutoRing", 0)
.getInt("DisconnectAction", mDisconnectAction);
mNotificationVolume = getSharedPreferences("AutoRing", 0)
.getInt("NotVol", mNotificationVolume);
mSvcStarted = true;
setForeground(true); //only has affect before 2.0. In 2.0 use startForeground()
} //onCreate
如果您使用的是 Android 2.0 或更高版本,您可以包含 onStartCommand
处理程序。这将允许您使用 Intent
对象在启动时将设置传递给服务,而不是使用共享的 static
值和 SharedPreferences
类。本演练兼容 1.5 版本,因此此代码已注释。
/*
@Override //only exists in 2.0 and later
public int onStartCommand(Intent intent, int flags, int startId) {
//add code here to get Intent data (user options)
//if service is killed and restarted by the OS, resend the Intent info
return START_REDELIVER_INTENT;
}
*/
添加电池状态更改的处理程序。我们服务的主要工作在此处完成。当手机断开连接时,电池状态变为 BATTERY_STATUS_DISCHARGING
。任何其他状态都表示手机已连接。当触发处理程序时,我们将根据用户设置更改响铃状态(响铃/关闭/振动)和通知(如 SMS)音量(零/非零)。然后,我们将通知音量存储在首选项中,以备服务重启。然后,如果 GUI 应用程序正在运行,我们将更新它。最后,我们更新手机状态栏中的通知图标和文本。
//the main listener for connect\disconnect
//we need to turn the main ringer on\off
//we also need to mute\ unmute the notification volume (ie Text Msg)
public BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive( Context context, Intent intent )
{
int newStatus = intent.getIntExtra( "status", 0 );
if (newStatus != mLastStatus) //status change
{
AudioManager am = (AudioManager)context.getSystemService
(Context.AUDIO_SERVICE);
//store max vol for later
int maxRingVol = am.getStreamMaxVolume(AudioManager.STREAM_RING);
//get current notification volume
int notVol = am.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
int curAction =
(newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING ||
newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) ?
mDisconnectAction : mConnectAction;
mBatteryStatus = //update text box
(newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING ||
newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING ?
"Not ": "") +"Connected ("+mActionList[curAction]+")";
if (curAction < 3) //skip if action=No Change
{
am.setRingerMode(curAction); //set main ringer
//change notification volume according to action
if (notVol > 0 && curAction < 2) //Silence or Vibrate
{
//store current volume for later
mNotificationVolume = notVol;
am.setStreamVolume(AudioManager.STREAM_RING, 0, 0); //force 0 volume
am.setStreamVolume( //set mute
AudioManager.STREAM_NOTIFICATION, 0, 0);
getSharedPreferences("AutoRing", 0)
.edit() //store for svc restart
.putInt("NotVol", mNotificationVolume).commit();
}
else //set ringer loud
{
if (curAction==2 && notVol==0) //if action=Ring
//and currently muted, reset volume
am.setStreamVolume(AudioManager.STREAM_NOTIFICATION,
mNotificationVolume, 0);
am.setStreamVolume( //force max volume
AudioManager.STREAM_RING, maxRingVol, 0);
}
}
//if the GUI app is connected, update app screen
if (mMainActivity!=null) mMainActivity.UpdateSvcStatus();
mLastStatus = newStatus;
//update status text and icon
DoNotify(!(newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING ||
newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING));
}
}}; //batteryReceiver
添加更新手机通知栏的方法。我们根据连接状态更改图标。我们根据用户设置和手机状态设置通知文本。使用 FLAG_ONGOING_EVENT
标志,这样我们的通知将位于 Ongoing 列表中,并且 Clear 按钮不会删除我们的消息。文本设置在状态栏中,位于我们的图标旁边,同时也是通知下拉列表中显示的文本。
private void DoNotify(boolean connected) //update notification text and icon
{
//set notification icon and text in status bar
Notification notification = new Notification (
connected? R.drawable.circlefill: R.drawable.circleopen,
mBatteryStatus, System.currentTimeMillis());
//set text in notification list
Intent notificationIntent = new Intent(this, AutoRingActivity.class);
notification.setLatestEventInfo(
getApplicationContext(), "AutoRing", mBatteryStatus,
PendingIntent.getActivity(this, 0, notificationIntent, 0));
notification.flags |= Notification.FLAG_ONGOING_EVENT; //Cannot be cleared
//trigger notification
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
.notify(1, notification);
} //DoNotify
应用程序启动并运行时,AutoRing
通知将如下所示。我们在通知列表中的文本中使用 mBatteryStatus
变量。这在 BroadcastReceiver
方法中设置。请注意,在我们的状态显示状态栏时,系统图标将被隐藏,以便我们的文本可以填满整个栏。如果消息超过栏的长度,消息将逐行滚动。您也可以在消息中使用“\n
”来显式引起滚动。
通过添加 onDestroy
处理程序来完成服务类。我们记录事件并显示一个弹出窗口以确认事件的发生。然后,我们取消注册电池事件监听器,然后取消通知,以便从状态栏和通知列表中删除图标和文本。似乎没有内置的方法来确定服务是否已启动/停止,因此我们明确设置 mSvcStarted
变量。我们在 onCreate
方法中将此变量设置为 true
。
当我们使用 AutoRingActivity
中的 stopService()
调用时,以及当用户使用 Android 设置中的“Running services”应用程序(Android 2.0 及更高版本)时,会调用 onDestroy
。当用户在 Android 设置中的“Manage applications”应用程序中使用 Force stop 或使用 adb kill(稍后讨论)时,不会调用 onDestroy
。
@Override //called when service is stopped
public void onDestroy() {
super.onDestroy();
android.util.Log.d("AutoRing","Service Stopping");
Toast.makeText(getApplicationContext(),
" Stopping ", Toast.LENGTH_SHORT).show();
unregisterReceiver(batteryReceiver); //disconnect listener
//remove notification icon and text
((NotificationManager) getSystemService
(Context.NOTIFICATION_SERVICE)).cancelAll();
mSvcStarted = false;
}
} //AutoRingSvc
这样,我们就完成了应用程序的编码。
构建项目(Project->Build All)。如果您设置了 Build Automatically,项目将在每次保存源文件时重新构建。
运行应用程序
要实际查看连接/断开连接功能,您需要将手机连接到计算机(虚拟设备始终连接)
- 使用 USB 线连接您的手机(您可能需要为您的手机安装 USB 驱动程序)
- 在您的手机上,在 Settings->Application->Development 中,启用 USB 调试
- 您的手机上的 USB Storage 应被禁用
在 Eclipse 中,按 F11 开始调试。
几秒钟后(如果一切顺利),应用程序应该会在您的手机上启动。
如果 Eclipse 启动了虚拟设备
- 关闭虚拟设备
- 转到 Run->Debug Configurations
- 在 Target 下的配置设置中,选择 Manual。这将允许您选择要调试的设备。
单击 Debug,然后在下一个屏幕中选择 Choose a running Android device,然后单击 OK 开始调试。
如果您没有看到任何正在运行的设备,请确认您的手机已连接到 PC 并且您有可用的 USB 数据线。您也可以下载 USBDeview(免费软件),它会列出 PC 上连接的 USB 设备。
要退出应用程序,请使用手机上的返回按钮或在 Eclipse 中选择 Run->Terminate。
要将应用程序安装到您的手机,请使用 APK 文件
在您的手机上,在 Settings->Applications 中,启用 Unknown sources 以允许手机安装非市场应用程序。
在 Eclipse 中,选择 File->Export..->Android-> Export Android Application。
单击“下一步”。
将 AutoRing
作为项目名称输入。
单击“下一步”。
如果您已有密钥库,请选择 Use existing keystore。如果没有,以下是创建密钥库的步骤。
选择 Create new keystore。输入文件名(不需要扩展名)和密码。
单击“下一步”。
对于 Alias 和 Password,您可以使用在上一屏幕中输入的值。将有效期设置为 100 年。在 Name 字段中输入任何名称。如果您计划使用此密钥库发布任何应用程序,您应该使用您的真实信息。
单击“下一步”。
输入您的 apk 文件的文件名。
单击“完成”。
要将 apk 文件安装到手机上,请使用 android-sdk\platform-tools 文件夹中的 adb 工具。如果您不知道文件夹位置,只需在计算机上搜索 adb.exe。
要安装 apk 文件,请使用此命令行
adb install C:\AutoRing.apk
您也可以使用 Android 市场上的(免费)安装程序应用程序,它允许您从手机的 SD 卡安装 apk 文件。
安装完成后,AutoRing
应可在您的手机应用程序列表中找到。
测试服务重启逻辑
如前所述,当我们在 stopService()
中使用 stopService()
调用以及当用户使用 Android 设置中的“Running services”应用程序(Android 2.0 及更高版本)时,会调用 onDestroy
。当用户在 Android 设置中的“Manage applications”应用程序中使用 Force stop 时,不会调用 onDestroy
。
如果您想模拟内存不足进程终止/重启,可以使用计算机上的 adb.exe
- 确保只有一个模拟器正在运行,并且您的手机未连接
- 在模拟器上启动
AutoRing
服务 - 从
cmd
提示符,转到 android-sdk\platform-tools 文件夹并运行 adb shell - 输入 ps。这将为您提供设备(或模拟器)上运行的进程列表。搜索名称为 droid.ar 的进程。这是
AutoRing
服务。记下进程的 PID(第二列)。 - 输入 kill pid,其中 pid 是 droid.ar 进程的 PID。这将立即导致服务被终止(不调用
onDestroy
)。它还将触发服务重启,这证实了重启逻辑正在工作。 - 输入 exit 退出 shell。
adb 不允许我在我的实际手机上杀死进程(Operation not permitted),因此我只在模拟器上进行了测试。
创建自己的图标
Android 应用程序使用的所有图标都是透明的 PNG 文件。Microsoft Paint(默认的 Windows 图像编辑器)不支持透明 PNG,因此如果您需要透明像素,您将需要使用其他应用程序。以下是 Photoshop 和 Paint.Net(免费图像编辑器)的说明。
Android 图标的一些基本规则:


- 图标名称必须是小写字母或数字。允许使用下划线( _ )。允许使用句点,但只有文件名的一部分将用作资源名称(myicon.abc.png 与 myicon.xyz.png 相同)。
- 通知图标(在状态栏中)为 25x25 像素,应为灰度(尽管支持颜色,如上面的蓝色图标)。
- 主应用程序图标为 72x72 像素,可以是全彩。我将其保存为 8 位颜色以减小文件大小。
- 箭头图标只是 Android 用于首选项的箭头副本。我使用了灰色版本,但彩色图标也可以工作(尽管在灰色按钮上可能看起来很奇怪)。我使用上面显示的绿色箭头测试了颜色。
- 选择 File->New
- 在“New”对话框中,在 Contents 下选择 Transparent 选项以允许透明度。设置所需的大小,然后单击 OK。棋盘格图案是图像的透明部分。
- 绘制您想要的内容,然后保存为 PNG 文件
- 如果您有现有图像并希望将背景设置为透明
- 打开现有图像
- 选择 Select->All,然后选择 Edit->Copy
- 打开一个具有 Transparent 选项的新图像
- 粘贴旧图像(Edit->Paste)
- 使用 Magic Wand Tool 选择您要设置为透明的像素,然后按 delete
- 您也可以使用 Background Eraser Tool 来清除特定像素
Paint.Net(这是免费软件,具有一些令人印象深刻的功能)。您可以在 这里 下载。
- 选择 File->New
- 设置所需的大小,然后单击 OK
- 选择 Edit->Select All 然后按 delete。这将使所有像素都设置为透明。
- 绘制您想要的图像,然后保存为 PNG 文件。我使用了 8 位颜色以减小文件大小。
- 如果您有现有图像并想将某些像素设置为透明
- 打开现有图像
- 使用 Magic Wand tool 选择您要删除的像素,然后按 delete
- 您也可以使用 Eraser tool 删除特定像素
完整的 Android 图标设计指南可以在 这里 找到。
其他想法
- 如果您只想测试监听器或通知,则不需要服务类。当 activity 移至后台时,监听器仍会运行。这方面的问题是,当内存不足时,操作系统会先删除后台 GUI 应用程序,然后再删除服务,并且 GUI 应用程序不会自动重启。
- 处理 1.5 和 2.0 之间的 API 更改可能很繁琐。如果您尝试编写向后兼容的应用程序,请做好准备。模糊的崩溃可能表示版本问题。
- 根据 版本分布图,97% 的 Android 用户使用 Android 2.1 及更高版本。在创建 Android 应用程序时,您可能可以忽略版本 1.5\1.6。
AutoRing
应用程序可能不带“No Change”选项会更好。如果用户选择此选项,状态文本将显示 No Change 而不是实际的响铃状态。当然,如果我们设置了实际的响铃状态,可能会让用户感到困惑。嗯。幸运的是,这个选项不切实际,而且很少使用。- 我只在我的 Ideos 2.2 上测试了重启逻辑。1.5\1.6\2.2 版本的虚拟机不允许我关闭设备。
- 在此应用程序中,我们使用上下文菜单来设置应用程序选项。如果您有很多选项要设置,请考虑使用
PreferenceActivity
类,其中使用单独的 activity 来设置用户首选项。 - 我无法在 1.5 模拟器中显示主应用程序图标。我尝试了几种图标版本。我认为这是模拟器问题。2.2 模拟器正确显示了图标。
- 虚拟机是获取应用程序屏幕截图的好方法。只需捕获/复制模拟器屏幕(Alt+PrtScr),然后使用图像编辑器进行裁剪。
- 主图标是使用 3D Studio(非常不免费)快速创建的,但如果您想尝试 3D 素材,有许多免费的 3D 建模工具可用。
- 如果您想要一个可以检测电池状况(以及许多其他状况)的完整应用程序,请查看 Android 市场中的 Llama 应用程序。它也是免费的。
- 我最近发现 Android 市场中有一个名称相似的应用程序。该应用程序与本演练无关。命名只是巧合。
资源
“从一个人那里抄袭,是抄袭;从两个人那里抄袭,是研究。” - Wilson Mizner
- Activity
- 服务
- BatteryManager
- AudioManager
- SharedPreferences
- Preference Modes
- 背景
- Creating Notifications
- Boot Service
- 计时器
- SystemClock
- TextView
- ContextMenu
- adb kill
- API Changes in 2.0
- Icon Guidelines
- Version Distribution
我想我们完成了。如果您仍在阅读本文,您对无聊的容忍度很高。我希望本演练对您有所帮助。如果您觉得有任何部分令人困惑,或者您认为我遗漏了什么,请告诉我,以便我更新此页面。