65.9K
CodeProject 正在变化。 阅读更多。
Home

AutoRing Android 服务创建演练

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (23投票s)

2011年8月21日

CPOL

19分钟阅读

viewsIcon

63527

downloadIcon

2876

创建一个监听连接更改的简单服务。

引言

本演练将涵盖服务应用程序的创建以及以下主题

  • 服务创建
  • 使用通知
  • 上下文菜单
  • 电池状态检查
  • 持久化用户设置
  • 创建图标

该项目是使用 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 用于首选项的箭头副本。我使用了灰色版本,但彩色图标也可以工作(尽管在灰色按钮上可能看起来很奇怪)。我使用上面显示的绿色箭头测试了颜色。
PhotoShop(我使用的是 7 版本,有点老)
  • 选择 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

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

© . All rights reserved.