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
 circlefill.png  circleopen.png
 circleopen.png  downarrow.png
 downarrow.png  mainicon.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
我想我们完成了。如果您仍在阅读本文,您对无聊的容忍度很高。我希望本演练对您有所帮助。如果您觉得有任何部分令人困惑,或者您认为我遗漏了什么,请告诉我,以便我更新此页面。




