android遇到的錯誤集
dialog.show()引起的android.view.WindowManager$BadTokenException錯誤
很多都是遇到過的問題,這裡集合一下,答案都是百度來的。
解決Content的startActivity方法報錯
BootBroadcastReceiver繼承自android.content.BroadcastReceiver,處理廣播事件,部分程式碼如下:
public void onReceive(Context context, Intent intent) { Intent startIPhone = new Intent(context, Iphone.class); context.startActivity(startIPhone); }
錯誤日誌:
02-10 13:26:11.017: DEBUG/AndroidRuntime(17173): Shutting down VM 02-10 13:26:11.017: WARN/dalvikvm(17173): threadid=1: thread exiting with uncaught exception (group=0x40015560) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): FATAL EXCEPTION: main 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): java.lang.RuntimeException: Unable to start receiver com.tmall.htc.BootBroadcastReceiver: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want? 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ActivityThread.handleReceiver(ActivityThread.java:1805) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ActivityThread.access$2400(ActivityThread.java:117) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:981) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.os.Handler.dispatchMessage(Handler.java:99) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.os.Looper.loop(Looper.java:123) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ActivityThread.main(ActivityThread.java:3683) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at java.lang.reflect.Method.invokeNative(Native Method) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at java.lang.reflect.Method.invoke(Method.java:507) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at dalvik.system.NativeStart.main(Native Method) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want? 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ContextImpl.startActivity(ContextImpl.java:621) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.content.ContextWrapper.startActivity(ContextWrapper.java:258) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.content.ContextWrapper.startActivity(ContextWrapper.java:258) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at com.tmall.htc.BootBroadcastReceiver.onReceive(BootBroadcastReceiver.java:64) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): at android.app.ActivityThread.handleReceiver(ActivityThread.java:1794) 02-10 13:26:11.048: ERROR/AndroidRuntime(17173): ... 10 more 02-10 13:26:11.078: WARN/ActivityManager(68): Force finishing activity com.tmall.htc/.Iphone
發生錯誤原因分析:
主要原因是
02-10 13:26:11.048: ERROR/AndroidRuntime(17173): Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
Content的startActivity方法,需要開啟一個新的task。
如果使用 Activity的startActivity方法,不會有任何限制,因為Activity繼承自Context,已過載了startActivity方法。
解決辦法:
public void onReceive(Context context, Intent intent) {
Intent startiPhone = new Intent(context, Iphone.class);
startiPhone.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startiPhone);
}
按照錯誤提示,新增一個FLAG_ACTIVITY_NEW_TASK flag
dialog.show()引起的android.view.WindowManager$BadTokenException錯誤
錯誤日誌
android.view.WindowManager$BadTokenException: Unable to add window -- token [email protected] not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:653)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:326)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:224)
at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:149)
at android.view.Window$LocalWindowManager.addView(Window.java:558)
at android.app.Dialog.show(Dialog.java:316)
錯誤原因
錯誤原因是Dialog在show的時候必須要有一個activity作為視窗載體,上面的日誌的意思是承載Dialog的activity已經被銷燬了,不存在了
解決辦法
1、在show之前加判斷activity是否被銷燬了
if(!isFinishing()){
dialog.show();
}
2、直接try catch(不推薦)
錯誤日誌
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
錯誤原因
先說說上下文的使用
對話方塊它是我們的Activity的一部分,對話方塊它掛載在我們的Activity上;
getApplicationContext()這個方法得到的是Context
Activity.this 得到Context的一個子類
也就是說 Activity.this 相當於是getApplicationContext()的子類
父類有的子類一定有 - 沒有 token
子類有的父類不一定有 --有 token
this 還有Activity.this和我們的getApplicationContext();
大多數情況推薦:Activity.this
解決辦法
上下文大多數情況推薦:Activity.this
dialog.dismiss()引起的java.lang.IllegalArgumentException錯誤
錯誤日誌
java.lang.IllegalArgumentException: View not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:383)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:285)
at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:104)
at android.app.Dialog.dismissDialog(Dialog.java:332)
at android.app.Dialog.dismiss(Dialog.java:315)
錯誤原因
這個錯誤測試是測不出來的,我是加了第三方的錯誤統計才得以發現的,原因是由於某種原因導致Activity被殺死後又重新建立
常發生這類Exception的情形都是,有一個費時的執行緒操作,需要在顯示一個ProgressDialog,在任務開始的時候顯示一個對話方塊,然後當任務完成了再Dismiss對話方塊,如果在此期間如果Activity因為某種原因被殺掉且又重新啟動了,那麼當Dismiss的時候WindowManager檢查發現Dialog所屬的Activity已經不存在了,所以會報IllegalArgumentException: View not attached to window manager.
解決辦法
從網上找了好些解決方案都不是太理想,然後就嘗試著自己解決, 我是這麼解決的,反正加上之後這個錯誤就沒有再出現過,如有不對還請賜教。
重寫Activity的onDestroy,將dialog置為空。
@Override
publicvoidonDestroy() {
super.onDestroy();
dialog=null;
}
讀取通訊錄時,使用者選擇拒絕,未能獲取許可權導致的java.lang.SecurityException: Permission Denial錯誤
錯誤日誌
java.lang.SecurityException: Permission Denial: reading com.android.providers.contacts.ContactsProvider2 uri content://com.android.contacts/data/phones from pid=27697, uid=10194 requires android.permission.READ_CONTACTS, or grantUriPermission()
at android.os.Parcel.readException(Parcel.java:1465)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:185)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:137)
at android.content.ContentProviderProxy.query(ContentProviderNative.java:413)
at android.content.ContentResolver.query(ContentResolver.java:470)
at android.content.ContentResolver.query(ContentResolver.java:413)
錯誤原因
讀取通訊錄時,使用者選擇拒絕,未能獲取許可權
解決辦法
直接try catch 如果捕獲到異常,提示使用者未授於許可權。
適配之圖片裁剪
http://www.jianshu.com/p/c73b959b6bcf
Android 7.0系統釋出後,拿到能升級的nexus 6P,就開始了7.0的適配。發現Android7.0在修改頭像時候進行拍照並裁剪圖片時會出現photos app崩潰。仔細分析操作步驟和流程,發現照片拍照是成功的,SD卡也能儲存相關的圖片資訊,但是在對拍照的圖片進行裁剪時候出現了photos app崩潰;如下圖:
同時發現通過選擇相簿進行選中圖片後才進行裁剪就沒有問題。看一下程式碼:
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(Uri.fromFile(inputfile), IMAGE_UNSPECIFIED);//主要問題就在這個File Uri上面 ————程式碼語句A
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 180);
intent.putExtra("outputY", 180);
intent.putExtra("scale", true);
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputfile));//定義輸出的File Uri,之後根據這個Uri去拿裁剪好的圖片資訊 ————程式碼B
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true);
startActivityForResult(intent, RequestCode);
上網查找了一些資料都是說Android7.0後,發現Android7.0在這個方面最大變化就在於以前適用的File Uri需要更改為Content Uri,詳情可以看這裡。但是這個更改是需要編譯的targetSdkVersion是24的時候才有效,我們的apptargetSdkVersion是23。所以問題上面的連結解決辦法應該不是這個原因(這個解決在後文解決targetSdkVersion=24的時候需要用到);
但是從文章中知道了File Uri和Content Uri的區別,然後再去分析一下為什麼從相簿選擇圖片進行裁剪是生效的?通過程式碼分析和debug後發現,從相簿選取圖片得到的Uri是Content Uri而拍照後使用的是檔案路徑生成的File Uri,看來問題就是出在這裡,並不是說我們app的targetSDKVersion不是24就可以使用File Uri,但是photos app的targetSdkVersion可能是24導致了它接受了File Uri而崩潰,那麼我們需要做的就是把File Uri換成Content Uri。這裡需要提的是,直接按照這裡的做法去更換Content Uri並不能生效,會提示“Can not edit image under 50*50 pixels”的錯誤toast提示,其實是photos app找不到Content Uri傳進去的圖片檔案。那麼我們需要換一種方式去更換Content Uri,我們在stackoverflow上面找到更換Content Uri的方法,需要注意的是不是所有的File Uri都可以轉換成Content Uri,應該是多媒體相關的檔案才可以。下面是程式碼:
public static Uri getImageContentUri(Context context, File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Images.Media._ID },
MediaStore.Images.Media.DATA + "=? ",
new String[] { filePath }, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA,filePath);returncontext.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
我們需要把上文第一處程式碼的Uri.fromFile(inputfile)更換成getImageContentUri生成的Uri。替換上去後執行程式試試,這樣修改就可以了。
更進一步:要是我們把上文的程式碼,把app的targetSdkVersion改成24之後,在啟動相機進行拍照時候,會發現這樣的錯誤
android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/Camera/TEMP_IMAGE1474468182889.jpg exposed beyond app through ClipData.Item.getUri();
不能拍照成功;這個時候我們就要用上文介紹的文章file:// scheme is now not allowed to be attached with Intent on targetSdkVersion 24 (Android Nougat).使用FileProvider來產生Content Uri代替File Uri,按照上面網址介紹方法替換掉就可以;
然而我們在用FileProvider.getUriForFile替換掉所有的Uri.fromFile時候,可以拍照成功了,但是在剪裁圖片時候還是會出現之前的“Can not edit image under 50*50 pixels”沒有辦法,只能把上文的程式碼語句A重新更改為getImageContentUri生成Content Uri,重新執行程式;這個時候可以拍照成功,進入圖片裁剪photos app裡面,但是裁剪完成Save的時候photos app又崩潰了:
應該是FileProvider的屬性為android:exported="false"的原因,但是這裡不能改為true(會報另一個錯誤:java.lang.RuntimeException: Unable to get provider android.support.v4.content.FileProvider: java.lang.SecurityException: Provider must not be exported)。
這個時候我們需要把程式碼語句B裡面outputUri改為File Uri就可以了,最終的程式碼如下,呼叫相機拍照的程式碼自己替換成FileProvider就可以了:
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(getImageContentUri(context , inputfile), IMAGE_UNSPECIFIED);//自己使用Content Uri替換File Uri
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 180);
intent.putExtra("outputY", 180);
intent.putExtra("scale", true);
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputfile));//定義輸出的File Uri
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true);
startActivityForResult(intent, RequestCode);
關於 Android 7.0 適配中 FileProvider 部分的總結
http://yifeng.studio/2017/05/03/android-7-0-compat-fileprovider/
常見使用場景
前面介紹的內容都是理論部分,在 開發者官方 FileProvider 部分 都有所介紹。接下來我們看看,實際開發一款應用的過程中,會經常遇見哪些 FileProvider 的使用場景。
自動安裝檔案
版本更新完成時開啟新版本 apk 檔案實現自動安裝的功能,應該是最常見的使用場景,也是每個應用必備功能之一。常見操作為,通知欄顯示下載新版本完畢,使用者點選或者監聽下載過程自動開啟新版本 apk 檔案。適配 Android 7.0 版本之前,我們程式碼可能是這樣:
File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-
archive");
startActivity(installIntent);
現在為了適配 7.0 及以上版本的系統,必須使用 Content URI 代替 File URI。
在 res/xml 目錄下新建一個 file_provider_paths.xml 檔案(檔名自由定義),並新增子目錄路徑資訊:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_download" path="Download"/>
</paths>
然後在 Manifest 檔案中註冊 FileProvider 物件,並連結上面的 path 路徑檔案:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.yifeng.samples.myprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"/>
</provider>
修改 java 程式碼,根據 File 物件生成 Content URI 物件,並授權訪問:
File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");
Uri apkUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID+".myprovider", apkFile);
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(installIntent);
如此這般,便完成了應用中呼叫系統功能開啟 apk 檔案的 7.0 適配工作。
呼叫系統拍照
呼叫系統拍照功能時也需要傳遞一個 Uri 物件,用於儲存圖片至指定目錄,這裡也需要適配 7.0 版本。其他步驟不再贅述,核心 java 程式碼如下(路徑不同,注意新增 res/xml 中的 path 檔案子目錄):
String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if(!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".myprovider", outputFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);
呼叫系統裁剪
呼叫系統裁剪的過程中涉及到兩個 Uri 物件:inputUri 和 outputUri,較為複雜一些。通常,呼叫系統裁剪的來源為呼叫系統拍照或選擇系統相簿。前者返回的是一個 File URI 物件,後者返回的是一個 Content URI 物件。作為裁剪源,我們要做的就是對其做進一步處理。但是不能像上面那樣使用 getUriForFile() 方法,這個並不難理解,因為如果是選擇系統相簿所得的圖片,本身也不一定屬於我們自己的應用。正確處理方式是這樣:
private Uri getImageContentUri(String path){
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
newString[]{MediaStore.Images.Media._ID},
MediaStore.Images.Media.DATA + "=? ",
newString[]{path}, null);
if(cursor != null && cursor.moveToFirst()) {
intid = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, ""+id);
} else {
ContentValues contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, path);
return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues);
}
}
拿到正確的 Content URI 後,作為 inputUri,傳遞給 Intent 物件:
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(inputUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
startActivityForResult(intent, REQUEST_PICK);
注意:這裡的 outputUri 並沒有改變,仍然使用的是 Uri.fromFile() 方法獲取的 File URI 型別!這是很奇怪的一點,但是不得不這麼做。事實上,使用這種方式呼叫系統裁剪功能本身就是有問題的!常見問題如:在部分機型上,呼叫系統裁剪並返回前一個頁面時,在 onActivityResult() 方法中得到的 resultCode 值不等於 RESULT_OK。Crop Intent 在官方文件中本來就無跡可尋,本身就是一種不推薦的用法!取而代之的是,我們可以使用 GitHub 上的一些開源庫實現應用內的圖片裁剪功能,比如 uCrop、cropper 等。