Merge changes from topic "avatarpicker1"
* changes: Offer colored default avatars if no preselected images are provided. Support taking or choosing a photo in avatar picker. Add avatar picker that shows a predefined set of images.
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.internal.util;
|
||||
|
||||
import android.annotation.ColorInt;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
@@ -72,9 +73,30 @@ public class UserIcons {
|
||||
// Return colored icon instead
|
||||
colorResId = USER_ICON_COLORS[userId % USER_ICON_COLORS.length];
|
||||
}
|
||||
return getDefaultUserIconInColor(resources, resources.getColor(colorResId, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default user icon in a particular color.
|
||||
*
|
||||
* @param resources resources object to fetch the user icon
|
||||
* @param color the color used for the icon
|
||||
*/
|
||||
public static Drawable getDefaultUserIconInColor(Resources resources, @ColorInt int color) {
|
||||
Drawable icon = resources.getDrawable(R.drawable.ic_account_circle, null).mutate();
|
||||
icon.setColorFilter(resources.getColor(colorResId, null), Mode.SRC_IN);
|
||||
icon.setColorFilter(color, Mode.SRC_IN);
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing colors to be used for default user icons.
|
||||
*/
|
||||
public static int[] getUserIconColors(Resources resources) {
|
||||
int[] result = new int[USER_ICON_COLORS.length];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = resources.getColor(USER_ICON_COLORS[i], null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ android_library {
|
||||
"SettingsLibSettingsTransition",
|
||||
"SettingsLibActivityEmbedding",
|
||||
"SettingsLibButtonPreference",
|
||||
"setupdesign",
|
||||
],
|
||||
|
||||
// ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES
|
||||
|
||||
@@ -18,4 +18,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.settingslib">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.android.settingslib.users.AvatarPickerActivity"
|
||||
android:theme="@style/SudThemeGlifV2.DayNight"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:attr/colorPrimary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="@dimen/avatar_picker_icon_inset"
|
||||
android:right="@dimen/avatar_picker_icon_inset"
|
||||
android:top="@dimen/avatar_picker_icon_inset"
|
||||
android:bottom="@dimen/avatar_picker_icon_inset"
|
||||
android:drawable="@drawable/ic_avatar_choose_photo"/>
|
||||
</layer-list>
|
||||
25
packages/SettingsLib/res/drawable/avatar_selector.xml
Normal file
25
packages/SettingsLib/res/drawable/avatar_selector.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:color="?android:attr/colorPrimary"
|
||||
android:width="@dimen/avatar_picker_padding"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:attr/colorPrimary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="@dimen/avatar_picker_icon_inset"
|
||||
android:right="@dimen/avatar_picker_icon_inset"
|
||||
android:top="@dimen/avatar_picker_icon_inset"
|
||||
android:bottom="@dimen/avatar_picker_icon_inset"
|
||||
android:drawable="@drawable/ic_avatar_take_photo"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorPrimary"
|
||||
android:pathData="M5.85,17.1q1.275,-0.975 2.85,-1.538Q10.275,15 12,15q1.725,0 3.3,0.563 1.575,0.562 2.85,1.537 0.875,-1.025 1.363,-2.325Q20,13.475 20,12q0,-3.325 -2.337,-5.662Q15.325,4 12,4T6.338,6.338Q4,8.675 4,12q0,1.475 0.487,2.775 0.488,1.3 1.363,2.325zM12,13q-1.475,0 -2.488,-1.012Q8.5,10.975 8.5,9.5t1.012,-2.487Q10.525,6 12,6t2.488,1.013Q15.5,8.024 15.5,9.5t-1.012,2.488Q13.475,13 12,13zM12,22q-2.075,0 -3.9,-0.788 -1.825,-0.787 -3.175,-2.137 -1.35,-1.35 -2.137,-3.175Q2,14.075 2,12t0.788,-3.9q0.787,-1.825 2.137,-3.175 1.35,-1.35 3.175,-2.137Q9.925,2 12,2t3.9,0.788q1.825,0.787 3.175,2.137 1.35,1.35 2.137,3.175Q22,9.925 22,12t-0.788,3.9q-0.787,1.825 -2.137,3.175 -1.35,1.35 -3.175,2.137Q14.075,22 12,22zM12,20q1.325,0 2.5,-0.387 1.175,-0.388 2.15,-1.113 -0.975,-0.725 -2.15,-1.113Q13.325,17 12,17t-2.5,0.387q-1.175,0.388 -2.15,1.113 0.975,0.725 2.15,1.113Q10.675,20 12,20zM12,11q0.65,0 1.075,-0.425 0.425,-0.425 0.425,-1.075 0,-0.65 -0.425,-1.075Q12.65,8 12,8q-0.65,0 -1.075,0.425Q10.5,8.85 10.5,9.5q0,0.65 0.425,1.075Q11.35,11 12,11zM12,9.5zM12,18.5z"/>
|
||||
</vector>
|
||||
|
||||
24
packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml
Normal file
24
packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorPrimary"
|
||||
android:pathData="M9,14h10l-3.45,-4.5 -2.3,3 -1.55,-2zM8,18q-0.825,0 -1.412,-0.587Q6,16.825 6,16L6,4q0,-0.825 0.588,-1.413Q7.175,2 8,2h12q0.825,0 1.413,0.587Q22,3.175 22,4v12q0,0.825 -0.587,1.413Q20.825,18 20,18zM8,16h12L20,4L8,4v12zM4,22q-0.825,0 -1.413,-0.587Q2,20.825 2,20L2,6h2v14h14v2zM8,4v12L8,4z"/>
|
||||
</vector>
|
||||
24
packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml
Normal file
24
packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorPrimary"
|
||||
android:pathData="M12,17.5q1.875,0 3.188,-1.313Q16.5,14.876 16.5,13q0,-1.875 -1.313,-3.188Q13.876,8.5 12,8.5q-1.875,0 -3.188,1.313Q7.5,11.124 7.5,13q0,1.875 1.313,3.188Q10.124,17.5 12,17.5zM4,21q-0.825,0 -1.413,-0.587Q2,19.825 2,19L2,7q0,-0.825 0.587,-1.412Q3.175,5 4,5h3.15L9,3h6l1.85,2L20,5q0.825,0 1.413,0.588Q22,6.175 22,7v12q0,0.825 -0.587,1.413Q20.825,21 20,21zM20,19L20,7L4,7v12zM4,19L4,7v12z"/>
|
||||
</vector>
|
||||
24
packages/SettingsLib/res/layout/avatar_item.xml
Normal file
24
packages/SettingsLib/res/layout/avatar_item.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/avatar_image"
|
||||
android:layout_height="@dimen/avatar_size_in_picker"
|
||||
android:layout_width="@dimen/avatar_size_in_picker"
|
||||
android:layout_margin="@dimen/avatar_picker_margin"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/avatar_picker_padding"
|
||||
android:background="@drawable/avatar_selector"/>
|
||||
38
packages/SettingsLib/res/layout/avatar_picker.xml
Normal file
38
packages/SettingsLib/res/layout/avatar_picker.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<com.google.android.setupdesign.GlifLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/glif_layout"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:icon="@drawable/ic_account_circle_outline"
|
||||
app:sucUsePartnerResource="true"
|
||||
app:sucHeaderText="@string/avatar_picker_title">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
style="@style/SudContentFrame">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/avatar_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.setupdesign.GlifLayout>
|
||||
21
packages/SettingsLib/res/values-w1280dp-land/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w1280dp-land/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">104dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">3dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w1440dp-land/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w1440dp-land/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">128dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">4dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w1600dp-land/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w1600dp-land/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">148dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">6dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w480dp-port/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w480dp-port/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">3</integer>
|
||||
<dimen name="avatar_size_in_picker">112dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">3dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w600dp-port/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w600dp-port/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">104dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">3dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w720dp-port/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w720dp-port/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">128dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">4dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w840dp-port/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w840dp-port/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">148dp</dimen>
|
||||
<dimen name="avatar_picker_padding">8dp</dimen>
|
||||
<dimen name="avatar_picker_margin">6dp</dimen>
|
||||
</resources>
|
||||
21
packages/SettingsLib/res/values-w960dp-land/dimens.xml
Normal file
21
packages/SettingsLib/res/values-w960dp-land/dimens.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<integer name="avatar_picker_columns">4</integer>
|
||||
<dimen name="avatar_size_in_picker">96dp</dimen>
|
||||
<dimen name="avatar_picker_padding">6dp</dimen>
|
||||
<dimen name="avatar_picker_margin">2dp</dimen>
|
||||
</resources>
|
||||
@@ -647,4 +647,6 @@
|
||||
<item>disabled</item>
|
||||
</array>
|
||||
|
||||
<array name="avatar_images"/>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -102,4 +102,11 @@
|
||||
<dimen name="user_photo_size_in_profile_info_dialog">112dp</dimen>
|
||||
<dimen name="add_a_photo_icon_size_in_profile_info_dialog">32dp</dimen>
|
||||
|
||||
<integer name="avatar_picker_columns">3</integer>
|
||||
<dimen name="avatar_size_in_picker">96dp</dimen>
|
||||
<dimen name="avatar_picker_padding">6dp</dimen>
|
||||
<dimen name="avatar_picker_margin">2dp</dimen>
|
||||
|
||||
<dimen name="avatar_picker_icon_inset">25dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1548,4 +1548,7 @@
|
||||
|
||||
<!-- Content description of the no calling for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
|
||||
<string name="accessibility_no_calling">No calling.</string>
|
||||
|
||||
<!-- Title for a screen allowing the user to choose a profile picture. [CHAR LIMIT=NONE] -->
|
||||
<string name="avatar_picker_title">Choose a profile picture</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.users;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.StrictMode;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.EventLog;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import libcore.io.Streams;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
class AvatarPhotoController {
|
||||
private static final String TAG = "AvatarPhotoController";
|
||||
|
||||
private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001;
|
||||
private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
|
||||
private static final int REQUEST_CODE_CROP_PHOTO = 1003;
|
||||
// in rare cases we get a null Cursor when querying for DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI
|
||||
// so we need a default photo size
|
||||
private static final int DEFAULT_PHOTO_SIZE = 500;
|
||||
|
||||
private static final String IMAGES_DIR = "multi_user";
|
||||
private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg";
|
||||
private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg";
|
||||
|
||||
private final int mPhotoSize;
|
||||
|
||||
private final AvatarPickerActivity mActivity;
|
||||
private final String mFileAuthority;
|
||||
|
||||
private final File mImagesDir;
|
||||
private final Uri mCropPictureUri;
|
||||
private final Uri mTakePictureUri;
|
||||
|
||||
AvatarPhotoController(AvatarPickerActivity activity, boolean waiting, String fileAuthority) {
|
||||
mActivity = activity;
|
||||
mFileAuthority = fileAuthority;
|
||||
|
||||
mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
|
||||
mImagesDir.mkdir();
|
||||
mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting);
|
||||
mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting);
|
||||
mPhotoSize = getPhotoSize(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles activity result from containing activity/fragment after a take/choose/crop photo
|
||||
* action result is received.
|
||||
*/
|
||||
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return false;
|
||||
}
|
||||
final Uri pictureUri = data != null && data.getData() != null
|
||||
? data.getData() : mTakePictureUri;
|
||||
|
||||
// Check if the result is a content uri
|
||||
if (!ContentResolver.SCHEME_CONTENT.equals(pictureUri.getScheme())) {
|
||||
Log.e(TAG, "Invalid pictureUri scheme: " + pictureUri.getScheme());
|
||||
EventLog.writeEvent(0x534e4554, "172939189", -1, pictureUri.getPath());
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_CROP_PHOTO:
|
||||
mActivity.returnUriResult(pictureUri);
|
||||
return true;
|
||||
case REQUEST_CODE_TAKE_PHOTO:
|
||||
case REQUEST_CODE_CHOOSE_PHOTO:
|
||||
if (mTakePictureUri.equals(pictureUri)) {
|
||||
if (PhotoCapabilityUtils.canCropPhoto(mActivity)) {
|
||||
cropPhoto();
|
||||
} else {
|
||||
onPhotoNotCropped(pictureUri);
|
||||
}
|
||||
} else {
|
||||
copyAndCropPhoto(pictureUri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void takePhoto() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE);
|
||||
appendOutputExtra(intent, mTakePictureUri);
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
|
||||
}
|
||||
|
||||
void choosePhoto() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES, null);
|
||||
intent.setType("image/*");
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO);
|
||||
}
|
||||
|
||||
private void copyAndCropPhoto(final Uri pictureUri) {
|
||||
// TODO: Replace AsyncTask
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
final ContentResolver cr = mActivity.getContentResolver();
|
||||
try (InputStream in = cr.openInputStream(pictureUri);
|
||||
OutputStream out = cr.openOutputStream(mTakePictureUri)) {
|
||||
Streams.copy(in, out);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to copy photo", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (!mActivity.isFinishing() && !mActivity.isDestroyed()) {
|
||||
cropPhoto();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void cropPhoto() {
|
||||
// TODO: Use a public intent, when there is one.
|
||||
Intent intent = new Intent("com.android.camera.action.CROP");
|
||||
intent.setDataAndType(mTakePictureUri, "image/*");
|
||||
appendOutputExtra(intent, mCropPictureUri);
|
||||
appendCropExtras(intent);
|
||||
if (intent.resolveActivity(mActivity.getPackageManager()) != null) {
|
||||
try {
|
||||
StrictMode.disableDeathOnFileUriExposure();
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO);
|
||||
} finally {
|
||||
StrictMode.enableDeathOnFileUriExposure();
|
||||
}
|
||||
} else {
|
||||
onPhotoNotCropped(mTakePictureUri);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendOutputExtra(Intent intent, Uri pictureUri) {
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri));
|
||||
}
|
||||
|
||||
private void appendCropExtras(Intent intent) {
|
||||
intent.putExtra("crop", "true");
|
||||
intent.putExtra("scale", true);
|
||||
intent.putExtra("scaleUpIfNeeded", true);
|
||||
intent.putExtra("aspectX", 1);
|
||||
intent.putExtra("aspectY", 1);
|
||||
intent.putExtra("outputX", mPhotoSize);
|
||||
intent.putExtra("outputY", mPhotoSize);
|
||||
}
|
||||
|
||||
private void onPhotoNotCropped(final Uri data) {
|
||||
// TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
// Scale and crop to a square aspect ratio
|
||||
Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
|
||||
Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(croppedImage);
|
||||
Bitmap fullImage;
|
||||
try {
|
||||
InputStream imageStream = mActivity.getContentResolver()
|
||||
.openInputStream(data);
|
||||
fullImage = BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
return null;
|
||||
}
|
||||
if (fullImage != null) {
|
||||
int rotation = getRotation(mActivity, data);
|
||||
final int squareSize = Math.min(fullImage.getWidth(),
|
||||
fullImage.getHeight());
|
||||
final int left = (fullImage.getWidth() - squareSize) / 2;
|
||||
final int top = (fullImage.getHeight() - squareSize) / 2;
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
RectF rectSource = new RectF(left, top,
|
||||
left + squareSize, top + squareSize);
|
||||
RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize);
|
||||
matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER);
|
||||
matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f);
|
||||
canvas.drawBitmap(fullImage, matrix, new Paint());
|
||||
return croppedImage;
|
||||
} else {
|
||||
// Bah! Got nothin.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
saveBitmapToFile(bitmap, new File(mImagesDir, CROP_PICTURE_FILE_NAME));
|
||||
mActivity.returnUriResult(mCropPictureUri);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the image's exif data and determines the rotation degree needed to display the image
|
||||
* in portrait mode.
|
||||
*/
|
||||
private int getRotation(Context context, Uri selectedImage) {
|
||||
int rotation = -1;
|
||||
try {
|
||||
InputStream imageStream = context.getContentResolver().openInputStream(selectedImage);
|
||||
ExifInterface exif = new ExifInterface(imageStream);
|
||||
rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
|
||||
} catch (IOException exception) {
|
||||
Log.e(TAG, "Error while getting rotation", exception);
|
||||
}
|
||||
|
||||
switch (rotation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBitmapToFile(Bitmap bitmap, File file) {
|
||||
try {
|
||||
OutputStream os = new FileOutputStream(file);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
|
||||
os.flush();
|
||||
os.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Cannot create temp file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getPhotoSize(Context context) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
|
||||
new String[]{ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) {
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0);
|
||||
} else {
|
||||
return DEFAULT_PHOTO_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Uri createTempImageUri(Context context, String fileName, boolean purge) {
|
||||
final File fullPath = new File(mImagesDir, fileName);
|
||||
if (purge) {
|
||||
fullPath.delete();
|
||||
}
|
||||
return FileProvider.getUriForFile(context, mFileAuthority, fullPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.users;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.internal.util.UserIcons;
|
||||
import com.android.settingslib.R;
|
||||
|
||||
import com.google.android.setupcompat.template.FooterBarMixin;
|
||||
import com.google.android.setupcompat.template.FooterButton;
|
||||
import com.google.android.setupdesign.GlifLayout;
|
||||
import com.google.android.setupdesign.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity to allow the user to choose a user profile picture.
|
||||
*
|
||||
* <p>Options are provided to take a photo or choose a photo using the photo picker. In addition,
|
||||
* preselected avatar images may be provided in the resource array {@code avatar_images}. If
|
||||
* provided, every element of that array must be a bitmap drawable.
|
||||
*
|
||||
* <p>If preselected images are not provided, the default avatar will be shown instead, in a range
|
||||
* of colors.
|
||||
*
|
||||
* <p>This activity should be started with startActivityForResult. If a photo or a preselected image
|
||||
* is selected, a Uri will be returned in the data field of the result intent. If a colored default
|
||||
* avatar is selected, the chosen color will be returned as {@code EXTRA_DEFAULT_ICON_TINT_COLOR}
|
||||
* and the data field will be empty.
|
||||
*/
|
||||
public class AvatarPickerActivity extends Activity {
|
||||
|
||||
static final String EXTRA_FILE_AUTHORITY = "file_authority";
|
||||
static final String EXTRA_DEFAULT_ICON_TINT_COLOR = "default_icon_tint_color";
|
||||
|
||||
private static final String KEY_AWAITING_RESULT = "awaiting_result";
|
||||
private static final String KEY_SELECTED_POSITION = "selected_position";
|
||||
|
||||
private boolean mWaitingForActivityResult;
|
||||
|
||||
private FooterButton mDoneButton;
|
||||
private AvatarAdapter mAdapter;
|
||||
|
||||
private AvatarPhotoController mAvatarPhotoController;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.trySetDynamicColor(this);
|
||||
setContentView(R.layout.avatar_picker);
|
||||
setUpButtons();
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.avatar_grid);
|
||||
mAdapter = new AvatarAdapter();
|
||||
recyclerView.setAdapter(mAdapter);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this,
|
||||
getResources().getInteger(R.integer.avatar_picker_columns)));
|
||||
|
||||
restoreState(savedInstanceState);
|
||||
|
||||
mAvatarPhotoController = new AvatarPhotoController(
|
||||
this, mWaitingForActivityResult, getFileAuthority());
|
||||
}
|
||||
|
||||
private void setUpButtons() {
|
||||
GlifLayout glifLayout = findViewById(R.id.glif_layout);
|
||||
FooterBarMixin mixin = glifLayout.getMixin(FooterBarMixin.class);
|
||||
|
||||
FooterButton secondaryButton =
|
||||
new FooterButton.Builder(this)
|
||||
.setText("Cancel")
|
||||
.setListener(view -> cancel())
|
||||
.build();
|
||||
|
||||
mDoneButton =
|
||||
new FooterButton.Builder(this)
|
||||
.setText("Done")
|
||||
.setListener(view -> mAdapter.returnSelectionResult())
|
||||
.build();
|
||||
mDoneButton.setEnabled(false);
|
||||
|
||||
mixin.setSecondaryButton(secondaryButton);
|
||||
mixin.setPrimaryButton(mDoneButton);
|
||||
}
|
||||
|
||||
private String getFileAuthority() {
|
||||
String authority = getIntent().getStringExtra(EXTRA_FILE_AUTHORITY);
|
||||
if (authority == null) {
|
||||
throw new IllegalStateException("File authority must be provided");
|
||||
}
|
||||
return authority;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
mWaitingForActivityResult = false;
|
||||
mAvatarPhotoController.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult);
|
||||
outState.putInt(KEY_SELECTED_POSITION, mAdapter.mSelectedPosition);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void restoreState(Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false);
|
||||
mAdapter.mSelectedPosition =
|
||||
savedInstanceState.getInt(KEY_SELECTED_POSITION, AvatarAdapter.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivityForResult(Intent intent, int requestCode) {
|
||||
mWaitingForActivityResult = true;
|
||||
super.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
void returnUriResult(Uri uri) {
|
||||
Intent resultData = new Intent();
|
||||
resultData.setData(uri);
|
||||
setResult(RESULT_OK, resultData);
|
||||
finish();
|
||||
}
|
||||
|
||||
void returnColorResult(int color) {
|
||||
Intent resultData = new Intent();
|
||||
resultData.putExtra(EXTRA_DEFAULT_ICON_TINT_COLOR, color);
|
||||
setResult(RESULT_OK, resultData);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
private class AvatarAdapter extends RecyclerView.Adapter<AvatarViewHolder> {
|
||||
|
||||
private static final int NONE = -1;
|
||||
|
||||
private final int mTakePhotoPosition;
|
||||
private final int mChoosePhotoPosition;
|
||||
private final int mPreselectedImageStartPosition;
|
||||
|
||||
private final List<Drawable> mImageDrawables;
|
||||
private final TypedArray mPreselectedImages;
|
||||
private final int[] mUserIconColors;
|
||||
private int mSelectedPosition = NONE;
|
||||
|
||||
AvatarAdapter() {
|
||||
final boolean canTakePhoto =
|
||||
PhotoCapabilityUtils.canTakePhoto(AvatarPickerActivity.this);
|
||||
final boolean canChoosePhoto =
|
||||
PhotoCapabilityUtils.canChoosePhoto(AvatarPickerActivity.this);
|
||||
mTakePhotoPosition = (canTakePhoto ? 0 : NONE);
|
||||
mChoosePhotoPosition = (canChoosePhoto ? (canTakePhoto ? 1 : 0) : NONE);
|
||||
mPreselectedImageStartPosition = (canTakePhoto ? 1 : 0) + (canChoosePhoto ? 1 : 0);
|
||||
|
||||
mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images);
|
||||
mUserIconColors = UserIcons.getUserIconColors(getResources());
|
||||
mImageDrawables = buildDrawableList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AvatarViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
||||
View itemView = layoutInflater.inflate(R.layout.avatar_item, parent, false);
|
||||
return new AvatarViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AvatarViewHolder viewHolder, int position) {
|
||||
if (position == mTakePhotoPosition) {
|
||||
viewHolder.setDrawable(getDrawable(R.drawable.avatar_take_photo_circled));
|
||||
viewHolder.setClickListener(view -> mAvatarPhotoController.takePhoto());
|
||||
|
||||
} else if (position == mChoosePhotoPosition) {
|
||||
viewHolder.setDrawable(getDrawable(R.drawable.avatar_choose_photo_circled));
|
||||
viewHolder.setClickListener(view -> mAvatarPhotoController.choosePhoto());
|
||||
|
||||
} else if (position >= mPreselectedImageStartPosition) {
|
||||
viewHolder.setSelected(position == mSelectedPosition);
|
||||
viewHolder.setDrawable(mImageDrawables.get(indexFromPosition(position)));
|
||||
viewHolder.setClickListener(view -> {
|
||||
if (mSelectedPosition == position) {
|
||||
deselect(position);
|
||||
} else {
|
||||
select(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mPreselectedImageStartPosition + mImageDrawables.size();
|
||||
}
|
||||
|
||||
private List<Drawable> buildDrawableList() {
|
||||
List<Drawable> result = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < mPreselectedImages.length(); i++) {
|
||||
Drawable drawable = mPreselectedImages.getDrawable(i);
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
result.add(circularDrawableFrom((BitmapDrawable) drawable));
|
||||
} else {
|
||||
throw new IllegalStateException("Avatar drawables must be bitmaps");
|
||||
}
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// No preselected images. Use tinted default icon.
|
||||
for (int i = 0; i < mUserIconColors.length; i++) {
|
||||
result.add(UserIcons.getDefaultUserIconInColor(getResources(), mUserIconColors[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Drawable circularDrawableFrom(BitmapDrawable drawable) {
|
||||
Bitmap bitmap = drawable.getBitmap();
|
||||
|
||||
RoundedBitmapDrawable roundedBitmapDrawable =
|
||||
RoundedBitmapDrawableFactory.create(getResources(), bitmap);
|
||||
roundedBitmapDrawable.setCircular(true);
|
||||
|
||||
return roundedBitmapDrawable;
|
||||
}
|
||||
|
||||
private int indexFromPosition(int position) {
|
||||
return position - mPreselectedImageStartPosition;
|
||||
}
|
||||
|
||||
private void select(int position) {
|
||||
final int oldSelection = mSelectedPosition;
|
||||
mSelectedPosition = position;
|
||||
notifyItemChanged(position);
|
||||
if (oldSelection != NONE) {
|
||||
notifyItemChanged(oldSelection);
|
||||
} else {
|
||||
mDoneButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void deselect(int position) {
|
||||
mSelectedPosition = NONE;
|
||||
notifyItemChanged(position);
|
||||
mDoneButton.setEnabled(false);
|
||||
}
|
||||
|
||||
private void returnSelectionResult() {
|
||||
int index = indexFromPosition(mSelectedPosition);
|
||||
if (mPreselectedImages.length() > 0) {
|
||||
int resourceId = mPreselectedImages.getResourceId(index, -1);
|
||||
if (resourceId == -1) {
|
||||
throw new IllegalStateException("Preselected avatar images must be resources.");
|
||||
}
|
||||
returnUriResult(uriForResourceId(resourceId));
|
||||
} else {
|
||||
returnColorResult(
|
||||
mUserIconColors[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri uriForResourceId(int resourceId) {
|
||||
return new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
.authority(getResources().getResourcePackageName(resourceId))
|
||||
.appendPath(getResources().getResourceTypeName(resourceId))
|
||||
.appendPath(getResources().getResourceEntryName(resourceId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AvatarViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ImageView mImageView;
|
||||
|
||||
AvatarViewHolder(View view) {
|
||||
super(view);
|
||||
mImageView = view.findViewById(R.id.avatar_image);
|
||||
}
|
||||
|
||||
public void setDrawable(Drawable drawable) {
|
||||
mImageView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
public void setClickListener(View.OnClickListener listener) {
|
||||
mImageView.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
mImageView.setSelected(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
@@ -36,6 +37,8 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.internal.util.UserIcons;
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
import com.android.settingslib.drawable.CircleFramedDrawable;
|
||||
|
||||
import java.io.File;
|
||||
@@ -139,12 +142,20 @@ public class EditUserInfoController {
|
||||
Drawable userIcon = getUserIcon(activity, defaultUserIcon);
|
||||
userPhotoView.setImageDrawable(userIcon);
|
||||
|
||||
if (canChangePhoto(activity)) {
|
||||
mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter,
|
||||
userPhotoView);
|
||||
} else {
|
||||
// some users can't change their photos, so we need to remove the suggestive icon
|
||||
if (isChangePhotoRestrictedByBase(activity)) {
|
||||
// some users can't change their photos so we need to remove the suggestive icon
|
||||
content.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE);
|
||||
} else {
|
||||
RestrictedLockUtils.EnforcedAdmin adminRestriction =
|
||||
getChangePhotoAdminRestriction(activity);
|
||||
if (adminRestriction != null) {
|
||||
userPhotoView.setOnClickListener(view ->
|
||||
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
|
||||
activity, adminRestriction));
|
||||
} else {
|
||||
mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter,
|
||||
userPhotoView);
|
||||
}
|
||||
}
|
||||
|
||||
mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon,
|
||||
@@ -203,16 +214,21 @@ public class EditUserInfoController {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean canChangePhoto(Context context) {
|
||||
return (PhotoCapabilityUtils.canCropPhoto(context)
|
||||
&& PhotoCapabilityUtils.canChoosePhoto(context))
|
||||
|| PhotoCapabilityUtils.canTakePhoto(context);
|
||||
boolean isChangePhotoRestrictedByBase(Context context) {
|
||||
return RestrictedLockUtilsInternal.hasBaseUserRestriction(
|
||||
context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) {
|
||||
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
|
||||
context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
EditUserPhotoController createEditUserPhotoController(Activity activity,
|
||||
ActivityStarter activityStarter, ImageView userPhotoView) {
|
||||
return new EditUserPhotoController(activity, activityStarter, userPhotoView,
|
||||
mSavedPhoto, mWaitingForActivityResult, mFileAuthority);
|
||||
mSavedPhoto, mFileAuthority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,46 +16,21 @@
|
||||
|
||||
package com.android.settingslib.users;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.StrictMode;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.ContactsContract.DisplayPhoto;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.EventLog;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListPopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import com.android.internal.util.UserIcons;
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
import com.android.settingslib.drawable.CircleFramedDrawable;
|
||||
|
||||
import libcore.io.Streams;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -63,8 +38,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* This class contains logic for starting activities to take/choose/crop photo, reads and transforms
|
||||
@@ -75,45 +49,30 @@ public class EditUserPhotoController {
|
||||
|
||||
// It seems that this class generates custom request codes and they may
|
||||
// collide with ours, these values are very unlikely to have a conflict.
|
||||
private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001;
|
||||
private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
|
||||
private static final int REQUEST_CODE_CROP_PHOTO = 1003;
|
||||
// in rare cases we get a null Cursor when querying for DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI
|
||||
// so we need a default photo size
|
||||
private static final int DEFAULT_PHOTO_SIZE = 500;
|
||||
private static final int REQUEST_CODE_PICK_AVATAR = 1004;
|
||||
|
||||
private static final String IMAGES_DIR = "multi_user";
|
||||
private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg";
|
||||
private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg";
|
||||
private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png";
|
||||
|
||||
private final int mPhotoSize;
|
||||
|
||||
private final Activity mActivity;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private final ImageView mImageView;
|
||||
private final String mFileAuthority;
|
||||
|
||||
private final File mImagesDir;
|
||||
private final Uri mCropPictureUri;
|
||||
private final Uri mTakePictureUri;
|
||||
|
||||
private Bitmap mNewUserPhotoBitmap;
|
||||
private Drawable mNewUserPhotoDrawable;
|
||||
|
||||
public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
|
||||
ImageView view, Bitmap bitmap, boolean waiting, String fileAuthority) {
|
||||
ImageView view, Bitmap bitmap, String fileAuthority) {
|
||||
mActivity = activity;
|
||||
mActivityStarter = activityStarter;
|
||||
mImageView = view;
|
||||
mFileAuthority = fileAuthority;
|
||||
|
||||
mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
|
||||
mImagesDir.mkdir();
|
||||
mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting);
|
||||
mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting);
|
||||
mPhotoSize = getPhotoSize(activity);
|
||||
mImageView.setOnClickListener(v -> showUpdatePhotoPopup());
|
||||
mImageView = view;
|
||||
mImageView.setOnClickListener(v -> showAvatarPicker());
|
||||
mNewUserPhotoBitmap = bitmap;
|
||||
}
|
||||
|
||||
@@ -125,32 +84,19 @@ public class EditUserPhotoController {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return false;
|
||||
}
|
||||
final Uri pictureUri = data != null && data.getData() != null
|
||||
? data.getData() : mTakePictureUri;
|
||||
|
||||
// Check if the result is a content uri
|
||||
if (!ContentResolver.SCHEME_CONTENT.equals(pictureUri.getScheme())) {
|
||||
Log.e(TAG, "Invalid pictureUri scheme: " + pictureUri.getScheme());
|
||||
EventLog.writeEvent(0x534e4554, "172939189", -1, pictureUri.getPath());
|
||||
return false;
|
||||
}
|
||||
if (requestCode == REQUEST_CODE_PICK_AVATAR) {
|
||||
if (data.hasExtra(AvatarPickerActivity.EXTRA_DEFAULT_ICON_TINT_COLOR)) {
|
||||
int tintColor =
|
||||
data.getIntExtra(AvatarPickerActivity.EXTRA_DEFAULT_ICON_TINT_COLOR, -1);
|
||||
onDefaultIconSelected(tintColor);
|
||||
return true;
|
||||
}
|
||||
if (data.getData() != null) {
|
||||
onPhotoCropped(data.getData());
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_CROP_PHOTO:
|
||||
onPhotoCropped(pictureUri);
|
||||
return true;
|
||||
case REQUEST_CODE_TAKE_PHOTO:
|
||||
case REQUEST_CODE_CHOOSE_PHOTO:
|
||||
if (mTakePictureUri.equals(pictureUri)) {
|
||||
if (PhotoCapabilityUtils.canCropPhoto(mActivity)) {
|
||||
cropPhoto();
|
||||
} else {
|
||||
onPhotoNotCropped(pictureUri);
|
||||
}
|
||||
} else {
|
||||
copyAndCropPhoto(pictureUri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -159,224 +105,60 @@ public class EditUserPhotoController {
|
||||
return mNewUserPhotoDrawable;
|
||||
}
|
||||
|
||||
private void showUpdatePhotoPopup() {
|
||||
final Context context = mImageView.getContext();
|
||||
final boolean canTakePhoto = PhotoCapabilityUtils.canTakePhoto(context);
|
||||
final boolean canChoosePhoto = PhotoCapabilityUtils.canChoosePhoto(context);
|
||||
|
||||
if (!canTakePhoto && !canChoosePhoto) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<EditUserPhotoController.RestrictedMenuItem> items = new ArrayList<>();
|
||||
|
||||
if (canTakePhoto) {
|
||||
final String title = context.getString(R.string.user_image_take_photo);
|
||||
items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
|
||||
this::takePhoto));
|
||||
}
|
||||
|
||||
if (canChoosePhoto) {
|
||||
final String title = context.getString(R.string.user_image_choose_photo);
|
||||
items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
|
||||
this::choosePhoto));
|
||||
}
|
||||
|
||||
final ListPopupWindow listPopupWindow = new ListPopupWindow(context);
|
||||
|
||||
listPopupWindow.setAnchorView(mImageView);
|
||||
listPopupWindow.setModal(true);
|
||||
listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
|
||||
listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items));
|
||||
|
||||
final int width = Math.max(mImageView.getWidth(), context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width));
|
||||
listPopupWindow.setWidth(width);
|
||||
listPopupWindow.setDropDownGravity(Gravity.START);
|
||||
|
||||
listPopupWindow.setOnItemClickListener((parent, view, position, id) -> {
|
||||
listPopupWindow.dismiss();
|
||||
final RestrictedMenuItem item =
|
||||
(RestrictedMenuItem) parent.getAdapter().getItem(position);
|
||||
item.doAction();
|
||||
});
|
||||
|
||||
listPopupWindow.show();
|
||||
private void showAvatarPicker() {
|
||||
Intent intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class);
|
||||
intent.putExtra(AvatarPickerActivity.EXTRA_FILE_AUTHORITY, mFileAuthority);
|
||||
mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR);
|
||||
}
|
||||
|
||||
private void takePhoto() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE);
|
||||
appendOutputExtra(intent, mTakePictureUri);
|
||||
mActivityStarter.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
|
||||
}
|
||||
private void onDefaultIconSelected(int tintColor) {
|
||||
try {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
Drawable drawable =
|
||||
UserIcons.getDefaultUserIconInColor(mActivity.getResources(), tintColor);
|
||||
Bitmap bitmap = convertToBitmap(drawable,
|
||||
(int) mActivity.getResources().getDimension(R.dimen.circle_avatar_size));
|
||||
|
||||
private void choosePhoto() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
|
||||
intent.setType("image/*");
|
||||
appendOutputExtra(intent, mTakePictureUri);
|
||||
mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO);
|
||||
}
|
||||
|
||||
private void copyAndCropPhoto(final Uri pictureUri) {
|
||||
// TODO: Replace AsyncTask
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
final ContentResolver cr = mActivity.getContentResolver();
|
||||
try (InputStream in = cr.openInputStream(pictureUri);
|
||||
OutputStream out = cr.openOutputStream(mTakePictureUri)) {
|
||||
Streams.copy(in, out);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to copy photo", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (!mActivity.isFinishing() && !mActivity.isDestroyed()) {
|
||||
cropPhoto();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void cropPhoto() {
|
||||
// TODO: Use a public intent, when there is one.
|
||||
Intent intent = new Intent("com.android.camera.action.CROP");
|
||||
intent.setDataAndType(mTakePictureUri, "image/*");
|
||||
appendOutputExtra(intent, mCropPictureUri);
|
||||
appendCropExtras(intent);
|
||||
if (intent.resolveActivity(mActivity.getPackageManager()) != null) {
|
||||
try {
|
||||
StrictMode.disableDeathOnFileUriExposure();
|
||||
mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO);
|
||||
} finally {
|
||||
StrictMode.enableDeathOnFileUriExposure();
|
||||
}
|
||||
} else {
|
||||
onPhotoNotCropped(mTakePictureUri);
|
||||
ThreadUtils.postOnMainThread(() -> onPhotoProcessed(bitmap));
|
||||
}).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.e(TAG, "Error processing default icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendOutputExtra(Intent intent, Uri pictureUri) {
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri));
|
||||
}
|
||||
|
||||
private void appendCropExtras(Intent intent) {
|
||||
intent.putExtra("crop", "true");
|
||||
intent.putExtra("scale", true);
|
||||
intent.putExtra("scaleUpIfNeeded", true);
|
||||
intent.putExtra("aspectX", 1);
|
||||
intent.putExtra("aspectY", 1);
|
||||
intent.putExtra("outputX", mPhotoSize);
|
||||
intent.putExtra("outputY", mPhotoSize);
|
||||
private static Bitmap convertToBitmap(@NonNull Drawable icon, int size) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
icon.setBounds(0, 0, size, size);
|
||||
icon.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void onPhotoCropped(final Uri data) {
|
||||
// TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
InputStream imageStream = null;
|
||||
try {
|
||||
imageStream = mActivity.getContentResolver()
|
||||
.openInputStream(data);
|
||||
return BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
Log.w(TAG, "Cannot find image file", fe);
|
||||
return null;
|
||||
} finally {
|
||||
if (imageStream != null) {
|
||||
try {
|
||||
imageStream.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Cannot close image stream", ioe);
|
||||
}
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
InputStream imageStream = null;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
imageStream = mActivity.getContentResolver()
|
||||
.openInputStream(data);
|
||||
bitmap = BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
Log.w(TAG, "Cannot find image file", fe);
|
||||
} finally {
|
||||
if (imageStream != null) {
|
||||
try {
|
||||
imageStream.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Cannot close image stream", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
onPhotoProcessed(bitmap);
|
||||
|
||||
if (bitmap != null) {
|
||||
Bitmap finalBitmap = bitmap;
|
||||
ThreadUtils.postOnMainThread(() -> onPhotoProcessed(finalBitmap));
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
private void onPhotoNotCropped(final Uri data) {
|
||||
// TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
// Scale and crop to a square aspect ratio
|
||||
Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
|
||||
Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(croppedImage);
|
||||
Bitmap fullImage;
|
||||
try {
|
||||
InputStream imageStream = mActivity.getContentResolver()
|
||||
.openInputStream(data);
|
||||
fullImage = BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
return null;
|
||||
}
|
||||
if (fullImage != null) {
|
||||
int rotation = getRotation(mActivity, data);
|
||||
final int squareSize = Math.min(fullImage.getWidth(),
|
||||
fullImage.getHeight());
|
||||
final int left = (fullImage.getWidth() - squareSize) / 2;
|
||||
final int top = (fullImage.getHeight() - squareSize) / 2;
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
RectF rectSource = new RectF(left, top,
|
||||
left + squareSize, top + squareSize);
|
||||
RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize);
|
||||
matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER);
|
||||
matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f);
|
||||
canvas.drawBitmap(fullImage, matrix, new Paint());
|
||||
return croppedImage;
|
||||
} else {
|
||||
// Bah! Got nothin.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
onPhotoProcessed(bitmap);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the image's exif data and determines the rotation degree needed to display the image
|
||||
* in portrait mode.
|
||||
*/
|
||||
private int getRotation(Context context, Uri selectedImage) {
|
||||
int rotation = -1;
|
||||
try {
|
||||
InputStream imageStream = context.getContentResolver().openInputStream(selectedImage);
|
||||
ExifInterface exif = new ExifInterface(imageStream);
|
||||
rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
|
||||
} catch (IOException exception) {
|
||||
Log.e(TAG, "Error while getting rotation", exception);
|
||||
}
|
||||
|
||||
switch (rotation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onPhotoProcessed(Bitmap bitmap) {
|
||||
@@ -386,29 +168,6 @@ public class EditUserPhotoController {
|
||||
.getInstance(mImageView.getContext(), mNewUserPhotoBitmap);
|
||||
mImageView.setImageDrawable(mNewUserPhotoDrawable);
|
||||
}
|
||||
new File(mImagesDir, TAKE_PICTURE_FILE_NAME).delete();
|
||||
new File(mImagesDir, CROP_PICTURE_FILE_NAME).delete();
|
||||
}
|
||||
|
||||
private static int getPhotoSize(Context context) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
|
||||
new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) {
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0);
|
||||
} else {
|
||||
return DEFAULT_PHOTO_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Uri createTempImageUri(Context context, String fileName, boolean purge) {
|
||||
final File fullPath = new File(mImagesDir, fileName);
|
||||
if (purge) {
|
||||
fullPath.delete();
|
||||
}
|
||||
return FileProvider.getUriForFile(context, mFileAuthority, fullPath);
|
||||
}
|
||||
|
||||
File saveNewUserPhotoBitmap() {
|
||||
@@ -435,84 +194,4 @@ public class EditUserPhotoController {
|
||||
void removeNewUserPhotoBitmapFile() {
|
||||
new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME).delete();
|
||||
}
|
||||
|
||||
private static final class RestrictedMenuItem {
|
||||
private final Context mContext;
|
||||
private final String mTitle;
|
||||
private final Runnable mAction;
|
||||
private final RestrictedLockUtils.EnforcedAdmin mAdmin;
|
||||
// Restriction may be set by system or something else via UserManager.setUserRestriction().
|
||||
private final boolean mIsRestrictedByBase;
|
||||
|
||||
/**
|
||||
* The menu item, used for popup menu. Any element of such a menu can be disabled by admin.
|
||||
*
|
||||
* @param context A context.
|
||||
* @param title The title of the menu item.
|
||||
* @param restriction The restriction, that if is set, blocks the menu item.
|
||||
* @param action The action on menu item click.
|
||||
*/
|
||||
RestrictedMenuItem(Context context, String title, String restriction,
|
||||
Runnable action) {
|
||||
mContext = context;
|
||||
mTitle = title;
|
||||
mAction = action;
|
||||
|
||||
final int myUserId = UserHandle.myUserId();
|
||||
mAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context,
|
||||
restriction, myUserId);
|
||||
mIsRestrictedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
|
||||
restriction, myUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
void doAction() {
|
||||
if (isRestrictedByBase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRestrictedByAdmin()) {
|
||||
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin);
|
||||
return;
|
||||
}
|
||||
|
||||
mAction.run();
|
||||
}
|
||||
|
||||
boolean isRestrictedByAdmin() {
|
||||
return mAdmin != null;
|
||||
}
|
||||
|
||||
boolean isRestrictedByBase() {
|
||||
return mIsRestrictedByBase;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where
|
||||
* any element can be restricted by admin (profile owner or device owner).
|
||||
*/
|
||||
private static final class RestrictedPopupMenuAdapter extends ArrayAdapter<RestrictedMenuItem> {
|
||||
RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> items) {
|
||||
super(context, R.layout.restricted_popup_menu_item, R.id.text, items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
final View view = super.getView(position, convertView, parent);
|
||||
final RestrictedMenuItem item = getItem(position);
|
||||
final TextView text = (TextView) view.findViewById(R.id.text);
|
||||
final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon);
|
||||
|
||||
text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase());
|
||||
image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase()
|
||||
? ImageView.VISIBLE : ImageView.GONE);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ public class PhotoCapabilityUtils {
|
||||
|
||||
/**
|
||||
* Check if the current user can perform any activity for
|
||||
* android.intent.action.GET_CONTENT action for images.
|
||||
* ACTION_PICK_IMAGES action for images.
|
||||
* Returns false if the device is currently locked and
|
||||
* requires a PIN, pattern or password to unlock.
|
||||
*/
|
||||
public static boolean canChoosePhoto(Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.setType("image/*");
|
||||
boolean canPerformActivityForGetImage =
|
||||
context.getPackageManager().queryIntentActivities(intent, 0).size() > 0;
|
||||
|
||||
@@ -62,7 +62,7 @@ public class EditUserInfoControllerTest {
|
||||
@Mock
|
||||
private ActivityStarter mActivityStarter;
|
||||
|
||||
private boolean mCanChangePhoto;
|
||||
private boolean mPhotoRestrictedByBase;
|
||||
private Activity mActivity;
|
||||
private TestEditUserInfoController mController;
|
||||
|
||||
@@ -85,8 +85,8 @@ public class EditUserInfoControllerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean canChangePhoto(Context context) {
|
||||
return mCanChangePhoto;
|
||||
boolean isChangePhotoRestrictedByBase(Context context) {
|
||||
return mPhotoRestrictedByBase;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class EditUserInfoControllerTest {
|
||||
mActivity = spy(ActivityController.of(new FragmentActivity()).get());
|
||||
mActivity.setTheme(R.style.Theme_AppCompat_DayNight);
|
||||
mController = new TestEditUserInfoController();
|
||||
mCanChangePhoto = true;
|
||||
mPhotoRestrictedByBase = true;
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -260,7 +260,7 @@ public class EditUserInfoControllerTest {
|
||||
|
||||
@Test
|
||||
public void createDialog_canNotChangePhoto_nullPhotoController() {
|
||||
mCanChangePhoto = false;
|
||||
mPhotoRestrictedByBase = false;
|
||||
|
||||
mController.createDialog(mActivity, mActivityStarter, mCurrentIcon,
|
||||
"test", "title", null, null);
|
||||
|
||||
Reference in New Issue
Block a user