我們知道,Android系統的各個模塊提供了非常強大的功能(比如電話,電源和設置等),通過使用這些功能,應用程序可以表現的更強大,更靈活。不過,使用這些功能並不是無條件的,而是需要擁有一些權限。接下來,我們就開始講解另一個非常重要的知識點——應用程序權限聲明,其中主要包括應用程序的權限聲明,自定義應用程序的訪問權限和SDK版本限定。
1.<uses-permission>——應用程序的權限申請
權限 | 描述 |
android.permission.ACCESS_NETWORK_STATE | 允許應用程序訪問網絡狀態 |
android.permission.ACCESS_WIFI_STATE | 允許應用程序訪問WI-FI狀態信息 |
com.android.voicemail.permission.ADD_VOICEMALL | 允許應用程序往系統中添加一封語音郵件 |
android.permission.BATTERY_STATS | 允許應用程序更新手機電池統計信息 |
android.permission.BIND_APPWIDGET | 允許應用程序通知AppWidget服務哪個應用程序可以訪問AppWidget的數據 Launcher是使用此權限的一個實例 |
android.permission.BLUETOOTH | 允許應用程序連接一個已經配對的藍牙設備 |
android.permission.BLUETOOTH_ADMIN | 允許應用程序主動發現和配對藍牙設備 |
android.permission.BROADCAST_PACKAGE_REMOVED | 允許醫用程序發送應用程序包已經卸載的通知 |
android.permission.BROADCAST_SMS | 允許應用程序廣播短信回執通知 |
android.permission.BROADCAST_STICKY | 允許應用程序廣播Sticky Intent。 有些廣播的數據在其廣播完成后被放在系統中,這樣應用程序可以快速訪問它們的數據,而無需等到下一個廣播到來 |
android.permission.CALL_PHONE | 允許應用程序初始化一次電話呼叫 |
android.permission.CAMERA | 請求訪問攝像設備 |
android.permission.CHANGE_CONFIGURATION | 允許應用程序修改當前的配置,比如語言種類,屏幕方向等。比如,我們的設置模塊就使用了這個權限 |
android.permission.CHANGE_NEWWORK_STATE | 允許應用程序改變連接狀態 |
android.permission.CHANGE_WIFI_STATE | 允許應用程序改變WI-FI連接狀態 |
android.permission.DEVICE_POWER | 允許應用程序訪問底層設備電源管理 |
android.permission.EXPAND_STATUS_BAR | 允許應用程序展開或者收起狀態欄 |
android.permission.INSTALL_LOCATION_PROVIDER | 允許應用程序安裝一個數據提供者到本地管理器中 |
android.permission.INSTALL_PACKAGES | 允許應用程序安裝另一個應用程序 |
android.permission.INTERNET | 允許應用程序打開網絡。 如果希望開發一個和網絡相關的應用程序,那么首先應該考慮是否需要這個權限 |
android.permission.KILL_BACKGROUND_PROCESSES | 允許應用程序調用killBackgroundProcesses()方法 |
android.permission.MODIFY_PHONE_STATE | 允許修改電話狀態,但不包括撥打電話 |
android.permission.MOUNT_FORMAT_FILESYSTEMS | 允許應用程序格式化可移除的外部存儲設備 |
android.permission.MOUNT_UNMOUNT_FILESYSTEMS | 允許應用程序掛載或者卸載外部存儲設備 |
android.permission.NFC | 允許應用程序執行NFC的輸入輸出操作 |
android.permission.READ_CALENDAR | 允許應用程序讀取日歷的數據 |
android.permission.READ_CONTACTS | 允許應用城西讀取聯系人的數據 |
android.permission.READ_PHONE_STATE | 允許應用程序訪問電話狀態 |
android.permission.READ_SMS | 允許應用程序訪問短信信息 |
android.permission.RECEIVE_BOOT_COMPLETED | 允許應用程序在系統完成以后接受到android.intent.action.BOOT_COMPLETED廣播 |
android.permission.RECEIVE_MMS | 允許應用程序監控MMS |
android.permission.RECEIVE_SMS | 允許應用程序監控SMS |
android.permission.RECEIVE_WAP_PUSH | 允許應用程序監控WAP的推送信息 |
android.permission.SEND_SMS | 允許應用程序主動發送短息 |
android.permission.SET_TIME | 允許應用程序設置系統時間 |
android.permission.SET_TIME_ZONE | 允許應用程序設置系統時區 |
android.permission.SET_WALLPAPER | 允許應用程序設置桌面壁紙 |
android.permission.STATUS_BAR | 允許應用程序操作(打開,關閉,禁用)狀態欄和它的圖標 |
android.permission.VIBRATE | 允許應用程序允許應用程序訪問振動設備 |
android.permission.WAKE_LOCK | 允許應用程序使用電源管理器的屏幕鎖功能 |
android.permission.WRITE_CALENDAR | 允許用戶寫入日歷數據。如果我們只申請了這個權限,那么我們對日歷的數據只有寫權限沒有讀權限 |
android.permission.WRITE_CONTACTS | 允許用戶寫入聯系人數據,如果我們只申請了這個權限,那么我們對聯系人的數據只有寫入權限,沒有讀權限 |
android.permission.WRITE_EXTERNAL_STORAGE | 允許應用程序把數據寫入外部存儲設備 |
android.permission.WRITE_SETTINGS | 允許應用程序讀寫系統設置 |
android.permission.WRITE_SMS | 允許應用程序寫短信 |
應用程序在不同的場景下可能需要上表所示的某些權限,比如當我們需要使用SD卡時,則需要申請SD卡相關權限。下面我們舉例來解釋這個問題。
在這個實例中,我們將改造HelloWorld應用程序,並在sdcard的根目錄下添加一個名為“abc.txt”的文本文件。由於需要訪問外部存儲設器,因此需要申請android.permission.WRITE_EXTERNAL_STORAGE權限,否則代碼將會失敗。具體步驟如下所示。
①需要在HelloWorld應用程序的AndroidManifest.xml文件中添加相應的權限,如下列代碼所示:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
②要在原來的代碼中添加一些創建文件的代碼,如下所示:
public class MainActivity extends FragmentActivity { private static final String SDCARD= Environment.getExternalStorageDirectory()+File.separator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(savedInstanceState==null){ getSupportFragmentManager().beginTransaction().add(android.R.id.content,new FileFragment()).commit(); } } public static class FileFragment extends Fragment { public FileFragment(){} @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view=inflater.inflate(R.layout.file_fragment,container,false); Button mybut= (Button) view.findViewById(R.id.mybut); mybut.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { File sdcardFile=new File(SDCARD+"abc.txt"); try { sdcardFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } }); return view; } } }
③啟動程序,這時在sdcard所鏈接的目錄下,我們會發現已經建立了abc.txt文件,如下圖所示。
最后我們來做一個實驗,將AndroidManifest.xml文件中的<uses-permission>節點刪除,在相同的目錄下並沒有發現所創建的文件,這樣操作之后,我們就能在日志里面發現一些異常信息,如下圖所示。
從日志中我們可以發現程序拋出了java.io.IOException異常,並且提示“Permission denied”,而它發生的地方,正好是創建文件的地方。因此,我們可以得出一個結論,在試圖讀寫外部存儲設備的時候,必須先要申請android.permission.WRITE_EXTERNAL_STORAGE這個權限,否則程序拋出警告性異常,相關操作也就無法進行。
Android提供了豐富的軟硬件功能模塊,它能讓應用程序變得強大,開發過程也更便捷,但在使用前,必須要為應用程序申請必要的權限。這點非常重要,否則應用程序就會出現莫名其妙的錯誤。
正如前面的實例中看到的結果,如果沒有相應的權限,就無法創建文件,而程序並沒有顯示一個異常的提示,這時我們就可能要花費大量的時間去找問題的根源。
因此,本人建議大家開發之前仔細分析需求,分析應用為什么功能,而這些動能是否需要權限才可以訪問。
2.<permission>節點——自定義應用程序的訪問權限
前面我們學習了如何使用權限。其實,應用程序除了可以使用權限之外,還可以定義自己的權限,用來限制對本應用程序或其他應用程序的特殊組件或功能訪問。在這里,我們來學習<permission>節點的作用——如何聲明自己的權限。
手動向AndroidManifest.xml文件中添加一個<permission>節點,它只能包含在<manifest>節點下,其語法如下所示:
<permission android:description="string resource"
android:icon="drawable resource"
android:logo="drawable resource"
android:label="string resource"
android:name="string"
android:permissionGroup="string"
android:protectionLevel=["normal"|"dangerous"|"signature"|"signatureOrSystem"]/>
以上代碼中,需要說明的有以下3個屬性。
①android:name:聲明權限的名稱。這個名稱必須是唯一的,因此,應該使用Java風格的命名,比如com.test.permission.TEST。
②android:permissionGroup:聲明權限從屬於哪一個權限組,這個權限組可以是Android預編譯的,也可以是自定義的。下表列除了Android系統預編譯的系統權限組。
權限組名稱 | 描述 |
android.permission-group.ACCOUNTS | 用於直接訪問由帳號管理器管理的帳號 |
android.permission-group.COST_MONEY | 用於使用戶不需要直接參與就可花錢的權限 |
android.permission-group.DEVELOPMENT_TOOLS | 與開發特征相關的權限群 |
android.permission-group.HARDWARE_CONTROLS | 用於提供直接訪問設備硬件的權限 |
android.permission-group.LOCATION | 用於允許用戶訪問用戶當前位置的權限 |
android.permission-group.MESSAGES | 用於允許應用程序以用戶的名義發送信息或者攔截用戶收到的信息的權限 |
android.permission-group.NETWORK | 適用於提供網絡服務的訪問權限 |
android.permission-group.PERSONAL_INFO | 適用於提供訪問到用戶私人數據的權限,如聯系人,日歷事件和電子郵件信息 |
android.permission-group.PHONE_CALLS | 適用於關聯訪問和修改電話狀態的權限,比如攔截去電,讀取和修改電話的狀態 |
android.permission-group.STORAGE | 與SD卡訪問有關的權限組 |
android.permission-group.SYSTEM_TOOLS | 與系統API有關的權限組 |
③android:protectionLevel:描述了隱含在權限中的潛在風險,該屬性的值可以是下表中的一個字符串。
值 | 意義 |
normal | 默認值。低風險的權限,它可以使請求的應用程序訪問孤立的應用程序級的功能,給其他應用程序,系統或者用戶帶來最小的風險。系統在安裝時,會自動授予這種類型的權限給請求的應用程序,無須用戶明確聲明。 |
dangerous | 高風險權限,它將事請求的應用程序能訪問用戶的私有數據或者控制那些會對用戶產生負面影響的設備。由於這種權限存在潛在的風險,系統可能不會自動被賦予請求的應用程序。例如,任何一個由應用程序請求的危險權限可能會顯示給用戶,並且在處理之前被要求確認。例如,以下權限就屬於此類權限。 <permission android:name="android.permission.RECEIVE_SMS" android:permissionGroup="android.permission-group.MESSAGES" android:protectionLevel="dangerous"/> 如果應用程序使用了這個權限,就有可能導致其他應用程序無法收到短信通知,此時我們認為使用這種權限是危險的。 |
signature | 簽名級別。系統只在請求的應用程序用同樣的簽名作為聲明權限時才授予該權限。如果認證匹配,則系統不通知用戶或者無須用戶明確批准就可以自動授權。 |
signatureOrSystem | 簽名或者系統級別。系統僅僅將它授給Android系統鏡像文件(*.img文件)中的應用程序,或者是和系統鏡像中的那些用同樣認證簽名的應用程序。一般情況下,盡量避免使用該選項,這是由於signature保護層級應滿足大多數需求和工程,而不管應用程序被確切地安裝在何處。signatureOrSystem權限適用於特定的特殊環境,在這樣的環境里,多個廠商已經將應用程序構建到系統鏡像中,並且需要明確共享特定特征。 |
在<permission>節點中,除了上面介紹的3個屬性外,還有其他一些屬性只是為了便於閱讀而存在,這里我們不在詳細介紹。
在Android系統提供的應用程序中,有一些定義了自己的權限,比如Launcher。下面的代碼片段用Launcher 的AndroidManifest.xml片段來說明如何手動聲明自己的權限:
<permission
android:name="com.android.launcher.permission.INSTALL_SHORT-CUT"
android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
android:protectionLevel="normal"
android:label="@string/permlab_install_shortcut"
android:description="@string/perdesc_install_shortcut"/>
因為聲明權限這一功能使用頻率比較低,因此讀者在開發應用程序的時候需要思考是否有必要聲明自己的權限。
3.<uses-sdk>節點——SDK版本限定
大家都知道,軟件對於平台版本是的一定要求的。如果平台版本能夠達到軟件運行的要求,那就能保證軟件的穩定性。比如,大家都知道NFC功能是不能在Android 1.5中運行的,如果正在開發帶類似功能的應用程序,那就必須對所在平台有所要求,而<uses-sdk>節點正是用來滿足這種需求的。
<uses-sdk>節點使用一個整型值來表達應用程序與一個或多個Android平台版本的兼容性。值得注意的是,這些整型值代表的是API級別。應用程序給定的API級別將和一個給定Android系統的API級別做比較。當然,對於不同的Android設備,這可能會有所不同。需要說明的是,這個節點用於指定API級別,而不是用於指定SDK的版本號或Android平台的。
使用此節點的代碼如下所示:
<uses-sdk android:minSdkVersion="integer"
android:targetSdkVersion="integer"
android:maxSdkVersion="integer"/>
①android:minSdkVersion:用於指定要運行應用程序所需的最小API級別。如果系統的API級別比該屬性指定的值要小,則Android系統會阻止用戶安裝此應用。在大多數情況下,應指定這個屬性。如果沒有指定該屬性,那么系統會認為此屬性值為“1”。
②android:targetSdkVersion:用於指定應用程序的目標API級別。
③android:maxSdkVersion:指定最大的API級別。
4.<instrumentation>節點——應用的監控器
<instrumentation>節點用於監控應用程序與系統交互,它會在應用程序組件實例化之前被實例化。這個節點在多數情況下用於單元測試。其語法結構如下:
<instrumentation android:functionalTest=["true"|"false"]
android:handleProfiling=["true"|"false"]
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:targetPackage="string">
下面詳細說明這些屬性:
①android:functionalTest:標識這個Instrumentation類是否作為一個功能測試運行,它的默認值是false。
②android:handleProfiling:標識這個Instrumentation對象是否打開和關閉性能分析,它的默認值是false。
③android:icon:這個屬性代表這個Instrumentation類的圖標
④android:label:該屬性是Instrumentation類的標題。
⑤android:name:該屬性是Instrumentation子類的名稱,是一個類的Java風格的名稱,例如com.example.liyuanjinglyj.myInstrumentation。
⑥android:targetPackage:該屬性是需要監控的目標應用程序名稱,這個名稱來自與目標應用程序AndroidManifest.xml中<manifest>節點的package屬性值。
當然在eclipse需要專門創建一個測試項目,而且還要配置這個節點,但是在android studio並不需要這么做,考慮到2015年底谷歌不再支持eclipse,所以只講解android studio單元測試,而講解這個節點是因為到現在eclipse依然有大量的開發者使用,所以,講解了節點的詳細說明。
下面舉例說明如何進行單元測試:
①創建項目,依然拿我們一直使用的HelloWorld做實驗,你會發現項目目錄里面有這樣一個測試包:
②右鍵點擊這個包,選擇NEW-CLASS菜單項,新建MyFirstTest繼承自ActivityInstrumentationTestCase2<T>,如下圖所示:
package com.example.liyuanjing.helloworld; import android.test.ActivityInstrumentationTestCase2; /** * Created by liyuanjing on 2015/7/14. */ public class MyFirstTest extends ActivityInstrumentationTestCase2<MainActivity>{ public MyFirstTest(){ super(MainActivity.class); } }
在這段代碼中,ActivityInstrumentationTestCase2<T>中的<T>要換成我們測試的類,因為我們要測試MainActivity所以換成這個。
③我們測試內容為,在編輯框中輸入一些字符,當點擊按鈕后,文本框等於編輯框輸入的字符串,MainActivity代碼如下:
public class MainActivity extends FragmentActivity { private static final String SDCARD= Environment.getExternalStorageDirectory()+File.separator; private FileFragment fileFragment=new FileFragment(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(savedInstanceState==null){ getSupportFragmentManager().beginTransaction().add(android.R.id.content,fileFragment).commit(); } } public static class FileFragment extends Fragment { private View mRootView; public FileFragment(){} @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view=inflater.inflate(R.layout.file_fragment,container,false); final EditText myEdit=(EditText)view.findViewById(R.id.myEdit); final TextView content=(TextView)view.findViewById(R.id.content); Button myBut=(Button)view.findViewById(R.id.myBut); myBut.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { content.setText(myEdit.getText().toString()); } }); this.mRootView=view; return view; } public View getRootView(){ return this.mRootView; } } public FileFragment getFragment(){ return fileFragment; } }
測試類的代碼如下:
public class MyFirstTest extends ActivityInstrumentationTestCase2<MainActivity>{ private TextView content; private EditText myEdit; private Button myBut; private MainActivity mainActivity; public MyFirstTest(){ super(MainActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); this.mainActivity=(MainActivity)getActivity(); this.content=(TextView)this.mainActivity.getFragment().getRootView().findViewById(R.id.content); this.myEdit=(EditText)this.mainActivity.getFragment().getRootView().findViewById(R.id.myEdit); this.myBut=(Button)this.mainActivity.getFragment().getRootView().findViewById(R.id.mybut); } public void testButtonClick(){ getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_H); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_E); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_L); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_L); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_O); getInstrumentation().waitForIdleSync(); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { myBut.performClick(); } }); getInstrumentation().waitForIdleSync(); SystemClock.sleep(1000); assertEquals(this.content.getText().toString(),this.myEdit.getText().toString()); } }
注意:
Ⅰ在MyFirstTest類的構造放中,由於指定了被測試項目的Activity的詳細信息,就使得MyFirstTest與Activity關聯上。
ⅡsetUp()方法是一個重寫的方法,它的作用是在測試前做必要的設置,比如實例化一些對象,打開網絡等,它與tearDown()方法成對出現。這里我們沒有實現tearDown()方法,當測試完成以后,框架會自動回調基類,也就是tearDown()方法。
最后,運行測試類,如下圖所示:
當系統完成測試后,就可以在Android studio集成開發環境中得到下圖所示的結果:
在圖中可以看到,編寫的Test測試類返回值為OK,這就證明測試達到了預期的目的。