Add conditionals to Settings

Also add Airplane Mode and Hotspot conditionals (more to come soon)

Change-Id: I11f206db59f7c715f416fb5852b8f0fcb857a247
This commit is contained in:
Jason Monk
2015-12-11 16:48:31 -05:00
parent a48f16fe01
commit db4ed191de
18 changed files with 1059 additions and 40 deletions

View File

@@ -2656,5 +2656,22 @@
</intent-filter>
</activity>
<!-- Conditional receivers, only enabled during silenced state, default off-->
<receiver
android:name=".dashboard.conditional.HotspotCondition$Receiver"
android:enabled="false">
<intent-filter>
<action android:name="android.net.wifi.WIFI_AP_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".dashboard.conditional.AirplaneModeCondition$Receiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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:name="root"
android:alpha="1.0"
android:height="48dp"
android:width="48dp"
android:viewportHeight="48"
android:viewportWidth="48" >
<group
android:name="ic_signal_airplane"
android:translateX="21.9995"
android:translateY="25.73401" >
<group
android:name="ic_signal_airplane_pivot"
android:translateX="-23.21545"
android:translateY="-18.86649" >
<clip-path
android:name="mask"
android:pathData="M 37.8337860107,-40.4599914551 c 0.0,0.0 -35.8077850342,31.5523681641 -35.8077850342,31.5523681641 c 0.0,0.0 9.55097961426,9.55285644531 9.55097961426,9.55285644531 c 0.0,0.0 -2.61698913574,2.09387207031 -2.61698913574,2.09387207031 c 0.0,0.0 -9.75096130371,-9.56428527832 -9.75096130371,-9.56428527832 c 0.0,0.0 -34.6200408936,25.4699249268 -34.6200408936,25.4699249268 c 0.0,0.0 55.9664764404,69.742401123 55.9664764404,69.742401123 c 0.0,0.0 73.2448120117,-59.1047973633 73.2448120117,-59.1047973633 c 0.0,0.0 -55.9664916992,-69.7423400879 -55.9664916992,-69.7423400879 Z" />
<group
android:name="cross" >
<path
android:name="cross_1"
android:pathData="M 7.54049682617,3.9430847168 c 0.0,0.0 0.324981689453,0.399978637695 0.324981689453,0.399978637695 "
android:strokeColor="#FFFFFFFF"
android:strokeAlpha="0"
android:strokeWidth="3.5"
android:fillColor="#00000000" />
</group>
<group
android:name="plane"
android:translateX="23.481"
android:translateY="18.71151" >
<path
android:name="plane_1"
android:pathData="M 18.9439849854,7.98849487305 c 0.0,0.0 0.0,-4.0 0.0,-4.0 c 0.0,0.0 -16.0,-10.0 -16.0,-10.0 c 0.0,0.0 0.0,-11.0 0.0,-11.0 c 0.0,-1.70001220703 -1.30000305176,-3.0 -3.0,-3.0 c -1.69999694824,0.0 -3.0,1.29998779297 -3.0,3.0 c 0.0,0.0 0.0,11.0 0.0,11.0 c 0.0,0.0 -16.0,10.0 -16.0,10.0 c 0.0,0.0 0.0,4.0 0.0,4.0 c 0.0,0.0 16.0,-5.0 16.0,-5.0 c 0.0,0.0 0.0,11.0 0.0,11.0 c 0.0,0.0 -4.0,3.0 -4.0,3.0 c 0.0,0.0 0.0,3.0 0.0,3.0 c 0.0,0.0 7.0,-2.0 7.0,-2.0 c 0.0,0.0 7.0,2.0 7.0,2.0 c 0.0,0.0 0.0,-3.0 0.0,-3.0 c 0.0,0.0 -4.0,-3.0 -4.0,-3.0 c 0.0,0.0 0.0,-11.0 0.0,-11.0 c 0.0,0.0 16.0,5.0 16.0,5.0 Z"
android:fillColor="#FFFFFFFF"
android:fillAlpha="1" />
</group>
</group>
</group>
</vector>

View File

@@ -19,10 +19,11 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="?android:attr/colorControlNormal"
android:fillColor="@android:color/white"
android:pathData="M12.0,8.0l-6.0,6.0 1.41,1.41L12.0,10.83l4.59,4.58L18.0,14.0z"/>
</vector>

View File

@@ -19,10 +19,11 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="?android:attr/colorControlNormal"
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12.0,13.17 7.41,8.59 6.0,10.0l6.0,6.0 6.0,-6.0z"/>
</vector>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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:name="root"
android:alpha="1.0"
android:height="48dp"
android:width="48dp"
android:viewportHeight="48"
android:viewportWidth="48" >
<group
android:name="ic_hotspot"
android:translateX="23.9778"
android:translateY="24.26443" >
<group
android:name="ic_hotspot_pivot"
android:translateX="-23.21545"
android:translateY="-18.86649" >
<clip-path
android:name="mask"
android:pathData="M 38.8337860107,-40.3974914551 c 0.0,0.0 -38.4077911377,30.8523712158 -38.4077911377,30.8523712158 c 0.0,0.0 6.97125244141,7.33258056641 6.97125244141,7.33258056641 c 0.0,0.0 -2.4169921875,2.57838439941 -2.4169921875,2.57838439941 c 0.0,0.0 -6.77128601074,-6.82850646973 -6.77128601074,-6.82850646973 c 0.0,0.0 -32.6199798584,25.1699066162 -32.6199798584,25.1699066162 c 0.0,0.0 55.9664764404,69.742401123 55.9664764404,69.742401123 c 0.0,0.0 27.6589050293,-22.6579437256 27.6589050293,-22.6579437256 c 0.0,0.0 -30.8645172119,-34.00390625 -30.8645172119,-34.00390625 c 0.0,0.0 2.70756530762,-1.99278259277 2.70756530762,-1.99278259277 c 0.0,0.0 1.53030395508,-0.876571655273 1.53030395508,-0.876571655274 c 0.0,0.0 2.85780334473,-3.12069702148 2.85780334473,-3.12069702148 c 0.0,0.0 0.659332275391,0.664688110352 0.659332275391,0.664688110351 c 0.0,0.0 -3.13299560547,2.82977294922 -3.13299560547,2.82977294922 c 0.0,0.0 29.0108337402,34.4080963135 29.0108337402,34.4080963135 c 0.0,0.0 42.8175811768,-34.3554534912 42.8175811768,-34.3554534912 c 0.0,0.0 -55.9664916992,-69.7423400879 -55.9664916992,-69.7423400879 Z" />
<group
android:name="cross" >
<path
android:name="cross_1"
android:pathData="M 4.44044494629,2.24310302734 c 0.0,0.0 0.0875396728516,0.112457275391 0.0875396728516,0.112457275391 "
android:strokeColor="#FFFFFFFF"
android:strokeAlpha="0"
android:strokeWidth="3.5"
android:fillColor="#00000000" />
</group>
<group
android:name="hotspot"
android:translateX="23.481"
android:translateY="18.71151" >
<group
android:name="circles"
android:translateX="-0.23909"
android:translateY="-0.10807" >
<path
android:name="path_3"
android:pathData="M -0.0042724609375,-2.64895629883 c -2.20922851562,0.0 -4.0,1.791015625 -4.0,4.0 c 0.0,2.20922851562 1.79077148438,4.0 4.0,4.0 c 2.208984375,0.0 4.0,-1.79077148438 4.0,-4.0 c 0.0,-2.208984375 -1.791015625,-4.0 -4.0,-4.0 Z M 11.9957275391,1.35104370117 c 0.0,-6.626953125 -5.373046875,-12.0 -12.0,-12.0 c -6.62719726562,0.0 -12.0,5.373046875 -12.0,12.0 c 0.0,4.43603515625 2.41381835938,8.30004882812 5.99194335938,10.3771972656 c 0.0,0.0 2.01586914062,-3.48217773438 2.01586914062,-3.48217773438 c -2.38500976562,-1.38500976562 -4.0078125,-3.93798828125 -4.0078125,-6.89501953125 c 0.0,-4.41796875 3.58178710938,-8.0 8.0,-8.0 c 4.41796875,0.0 8.0,3.58203125 8.0,8.0 c 0.0,2.95703125 -1.623046875,5.51000976562 -4.00805664062,6.89501953125 c 0.0,0.0 2.01586914062,3.48217773438 2.01586914062,3.48217773438 c 3.578125,-2.0771484375 5.9921875,-5.94116210938 5.9921875,-10.3771972656 Z M -0.0042724609375,-18.6489562988 c -11.0451660156,0.0 -20.0,8.9541015625 -20.0,20.0 c 0.0,7.39306640625 4.02099609375,13.8330078125 9.98779296875,17.2951660156 c 0.0,0.0 2.00219726562,-3.458984375 2.00219726562,-3.458984375 c -4.77319335938,-2.77001953125 -7.98999023438,-7.92211914062 -7.98999023438,-13.8361816406 c 0.0,-8.8369140625 7.16381835938,-16.0 16.0,-16.0 c 8.83595275879,0.0 16.0000152588,7.1630859375 16.0000152588,16.0 c 0.0,5.9140625 -3.21704101562,11.0661621094 -7.990234375,13.8361816406 c 0.0,0.0 2.00219726562,3.458984375 2.00219726563,3.458984375 c 5.966796875,-3.46215820312 9.98803710937,-9.90209960938 9.98803710938,-17.2951660156 c 0.0,-11.0458984375 -8.955078125,-20.0 -20.0000152588,-20.0 Z"
android:fillColor="#FFFFFFFF"
android:fillAlpha="1" />
</group>
</group>
</group>
</group>
</vector>

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:background="?android:attr/colorAccent"
android:elevation="3dp"
android:clickable="true">
<LinearLayout
android:id="@+id/collapsed_group"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:id="@android:id/icon"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_marginEnd="36dp"
android:tint="?android:attr/textColorPrimaryInverse" />
<TextView
android:id="@android:id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimaryInverse" />
<ImageView
android:id="@+id/expand_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="?android:attr/textColorPrimaryInverse"
android:padding="?android:attr/listPreferredItemPaddingEnd"
android:clickable="true"
android:background="?android:attr/selectableItemBackground" />
</LinearLayout>
<LinearLayout
android:id="@+id/detail_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="60dp"
android:orientation="vertical">
<TextView
android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="16dp"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorPrimaryInverse" />
<!-- TODO: Better background -->
<View
android:layout_width="match_parent"
android:layout_height=".25dp"
android:background="@android:color/white" />
<com.android.internal.widget.ButtonBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
style="?attr/buttonBarStyle"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<Button
android:id="@+id/first_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:textColor="?android:attr/textColorPrimaryInverse"
style="?android:attr/buttonBarButtonStyle" />
<Button
android:id="@+id/second_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimaryInverse"
style="?android:attr/buttonBarButtonStyle" />
</com.android.internal.widget.ButtonBarLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -14,7 +14,7 @@
limitations under the License.
-->
<android.support.v7.widget.RecyclerView
<com.android.settings.dashboard.conditional.FocusRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dashboard_container"
android:layout_width="match_parent"

View File

@@ -101,7 +101,7 @@
<dimen name="dashboard_category_padding_end">0dp</dimen>
<!-- Dashboard category panel elevation -->
<dimen name="dashboard_category_elevation">4dp</dimen>
<dimen name="dashboard_category_elevation">2dp</dimen>
<!-- Dashboard category title layout height -->
<dimen name="dashboard_category_title_height">48dp</dimen>

View File

@@ -6691,4 +6691,20 @@
<string name="default_organisation_name">organisation</string>
<string name="default_admin_support_msg">Contact them to learn more.</string>
<string name="list_of_administrators">List of administrators</string>
<!-- Turn off a conditional state of the device (e.g. airplane mode, or hotspot) [CHAR LIMIT=30] -->
<string name="condition_turn_off">Turn off</string>
<!-- Title of condition that hotspot is on [CHAR LIMIT=30] -->
<string name="condition_hotspot_title">Hotspot is on</string>
<!-- Summary of condition that hotspot is on [CHAR LIMIT=NONE] -->
<string name="condition_hotspot_summary">Portable Wi-Fi hotspot <xliff:g name="ap_name" example="AndroidAP">%1$s</xliff:g> is active, Wi-Fi for this device is turned off.</string>
<!-- Title of condition that airplane mode is on [CHAR LIMIT=30] -->
<string name="condition_airplane_title">Airplane mode is on</string>
<!-- Summary of condition that airplane mode is on [CHAR LIMIT=NONE] -->
<string name="condition_airplane_summary">Wi-Fi, Bluetooth, and cellular network are turned off. You can\'t make phone calls or connect to the Internet.</string>
</resources>

View File

@@ -74,6 +74,7 @@ import android.text.style.TtsSpan;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -83,8 +84,6 @@ import android.view.animation.AnimationUtils;
import android.widget.ListView;
import android.widget.TabWidget;
import com.android.internal.util.UserIcons;
import com.android.settingslib.drawer.UserAdapter;
import com.android.settingslib.drawer.UserAdapter.UserDetails;
import java.io.IOException;
import java.io.InputStream;
@@ -1058,5 +1057,11 @@ public final class Utils {
return UserHandle.myUserId();
}
}
public static int resolveResource(Context context, int attr) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(attr, value, true);
return value.resourceId;
}
}

View File

@@ -18,7 +18,6 @@ package com.android.settings.dashboard;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,40 +25,52 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.dashboard.conditional.Condition;
import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.DashboardTile;
import java.util.ArrayList;
import java.util.List;
public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> {
public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> implements View.OnClickListener {
public static final String TAG = "DashboardAdapter";
private final List<Object> mItems = new ArrayList<>();
private final List<Integer> mTypes = new ArrayList<>();
private final List<Integer> mIds = new ArrayList<>();
private final List<DashboardCategory> mCategories;
private final Context mContext;
private List<DashboardCategory> mCategories;
private List<Condition> mConditions;
private boolean mIsShowingAll;
// Used for counting items;
private int mId;
public DashboardAdapter(Context context, List<DashboardCategory> categories) {
private Condition mExpandedCondition = null;
public DashboardAdapter(Context context) {
mContext = context;
setHasStableIds(true);
}
public void setCategories(List<DashboardCategory> categories) {
mCategories = categories;
// TODO: Better place for tinting?
TypedValue tintColor = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
mContext.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
tintColor, true);
for (int i = 0; i < categories.size(); i++) {
for (int j = 0; j < categories.get(i).tiles.size(); j++) {
DashboardTile tile = categories.get(i).tiles.get(j);
if (!context.getPackageName().equals(
if (!mContext.getPackageName().equals(
tile.intent.getComponent().getPackageName())) {
// If this drawable is coming from outside Settings, tint it to match the
// color.
@@ -67,9 +78,12 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
}
}
setShowingAll(mIsShowingAll);
}
setShowingAll(false);
setHasStableIds(true);
public void setConditions(List<Condition> conditions) {
mConditions = conditions;
setShowingAll(mIsShowingAll);
}
public boolean isShowingAll() {
@@ -88,19 +102,21 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
public void setShowingAll(boolean showingAll) {
mIsShowingAll = showingAll;
reset();
countItem(null, com.android.settings.R.layout.dashboard_spacer, true);
for (int i = 0; i < mCategories.size(); i++) {
for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
countItem(mConditions.get(i), R.layout.condition_card, mConditions.get(i).shouldShow());
}
countItem(null, R.layout.dashboard_spacer, true);
for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
countItem(category, com.android.settings.R.layout.dashboard_category, mIsShowingAll);
countItem(category, R.layout.dashboard_category, mIsShowingAll);
for (int j = 0; j < category.tiles.size(); j++) {
DashboardTile tile = category.tiles.get(j);
Log.d(TAG, "Maybe adding " + tile.intent.getComponent().getClassName());
countItem(tile, com.android.settings.R.layout.dashboard_tile, mIsShowingAll
countItem(tile, R.layout.dashboard_tile, mIsShowingAll
|| ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
tile.intent.getComponent().getClassName()));
}
}
countItem(null, com.android.settings.R.layout.see_all, true);
countItem(null, R.layout.see_all, true);
notifyDataSetChanged();
}
@@ -129,10 +145,10 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
@Override
public void onBindViewHolder(DashboardItemHolder holder, int position) {
switch (mTypes.get(position)) {
case com.android.settings.R.layout.dashboard_category:
case R.layout.dashboard_category:
onBindCategory(holder, (DashboardCategory) mItems.get(position));
break;
case com.android.settings.R.layout.dashboard_tile:
case R.layout.dashboard_tile:
final DashboardTile tile = (DashboardTile) mItems.get(position);
onBindTile(holder, tile);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@@ -142,7 +158,7 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
});
break;
case com.android.settings.R.layout.see_all:
case R.layout.see_all:
onBindSeeAll(holder);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@@ -151,6 +167,16 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
});
break;
case R.layout.condition_card:
ConditionAdapterUtils.bindViews((Condition) mItems.get(position), holder,
mItems.get(position) == mExpandedCondition, this,
new View.OnClickListener() {
@Override
public void onClick(View v) {
onExpandClick(v);
}
});
break;
}
}
@@ -170,8 +196,8 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
private void onBindSeeAll(DashboardItemHolder holder) {
holder.title.setText(mIsShowingAll ? com.android.settings.R.string.see_less
: com.android.settings.R.string.see_all);
holder.title.setText(mIsShowingAll ? R.string.see_less
: R.string.see_all);
}
@Override
@@ -189,10 +215,38 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
return mIds.size();
}
@Override
public void onClick(View v) {
if (v.getTag() == mExpandedCondition) {
mExpandedCondition.onPrimaryClick();
} else {
mExpandedCondition = (Condition) v.getTag();
notifyDataSetChanged();
}
}
public void onExpandClick(View v) {
if (v.getTag() == mExpandedCondition) {
mExpandedCondition = null;
} else {
mExpandedCondition = (Condition) v.getTag();
}
notifyDataSetChanged();
}
public Object getItem(long itemId) {
for (int i = 0; i < mIds.size(); i++) {
if (mIds.get(i) == itemId) {
return mItems.get(i);
}
}
return null;
}
public static class DashboardItemHolder extends RecyclerView.ViewHolder {
private final ImageView icon;
private final TextView title;
private final TextView summary;
public final ImageView icon;
public final TextView title;
public final TextView summary;
public DashboardItemHolder(View itemView) {
super(itemView);

View File

@@ -18,7 +18,6 @@ package com.android.settings.dashboard;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -31,13 +30,17 @@ import com.android.settings.InstrumentedFragment;
import com.android.settings.R;
import com.android.settings.Settings;
import com.android.settings.SettingsActivity;
import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
import com.android.settings.dashboard.conditional.ConditionManager;
import com.android.settings.dashboard.conditional.FocusRecyclerView;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.SettingsDrawerActivity;
import java.util.List;
public class DashboardSummary extends InstrumentedFragment
implements SettingsDrawerActivity.CategoryListener {
implements SettingsDrawerActivity.CategoryListener, ConditionManager.ConditionListener,
FocusRecyclerView.FocusListener {
public static final boolean DEBUG = false;
private static final boolean DEBUG_TIMING = false;
private static final String TAG = "DashboardSummary";
@@ -51,11 +54,10 @@ public class DashboardSummary extends InstrumentedFragment
Settings.StorageSettingsActivity.class.getName(),
};
private static final int MSG_REBUILD_UI = 1;
private RecyclerView mDashboard;
private FocusRecyclerView mDashboard;
private DashboardAdapter mAdapter;
private SummaryLoader mSummaryLoader;
private ConditionManager mConditionManager;
@Override
protected int getMetricsCategory() {
@@ -73,6 +75,7 @@ public class DashboardSummary extends InstrumentedFragment
setHasOptionsMenu(true);
if (DEBUG_TIMING) Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime)
+ " ms");
mConditionManager = ConditionManager.get(getContext());
}
@Override
@@ -95,6 +98,7 @@ public class DashboardSummary extends InstrumentedFragment
((SettingsDrawerActivity) getActivity()).addCategoryListener(this);
mSummaryLoader.setListening(true);
Log.d(TAG, "onResume");
}
@Override
@@ -105,6 +109,16 @@ public class DashboardSummary extends InstrumentedFragment
mSummaryLoader.setListening(false);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (hasWindowFocus) {
mConditionManager.addListener(this);
mConditionManager.refreshAll();
} else {
mConditionManager.remListener(this);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -113,11 +127,16 @@ public class DashboardSummary extends InstrumentedFragment
@Override
public void onViewCreated(View view, Bundle bundle) {
mDashboard = (RecyclerView) view.findViewById(R.id.dashboard_container);
mDashboard = (FocusRecyclerView) view.findViewById(R.id.dashboard_container);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
llm.setOrientation(LinearLayoutManager.VERTICAL);
mDashboard.setLayoutManager(llm);
mDashboard.setHasFixedSize(true);
mDashboard.setListener(this);
mAdapter = new DashboardAdapter(getContext());
mAdapter.setConditions(mConditionManager.getConditions());
mSummaryLoader.setAdapter(mAdapter);
ConditionAdapterUtils.addDismiss(mDashboard);
rebuildUI();
}
@@ -132,10 +151,7 @@ public class DashboardSummary extends InstrumentedFragment
// TODO: Cache summaries from old categories somehow.
List<DashboardCategory> categories =
((SettingsActivity) getActivity()).getDashboardCategories();
boolean showingAll = mAdapter != null && mAdapter.isShowingAll();
mAdapter = new DashboardAdapter(getContext(), categories);
mSummaryLoader.setAdapter(mAdapter);
mAdapter.setShowingAll(showingAll);
mAdapter.setCategories(categories);
mDashboard.setAdapter(mAdapter);
long delta = System.currentTimeMillis() - start;
@@ -146,4 +162,10 @@ public class DashboardSummary extends InstrumentedFragment
public void onCategoriesChanged() {
rebuildUI();
}
@Override
public void onConditionsChanged() {
Log.d(TAG, "onConditionsChanged");
mAdapter.setConditions(mConditionManager.getConditions());
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2015 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.settings.dashboard.conditional;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.net.ConnectivityManager;
import com.android.settings.R;
import com.android.settings.Settings;
import com.android.settingslib.WirelessUtils;
public class AirplaneModeCondition extends Condition {
public AirplaneModeCondition(ConditionManager conditionManager) {
super(conditionManager);
}
@Override
public void refreshState() {
setActive(WirelessUtils.isAirplaneModeOn(mManager.getContext()));
}
@Override
protected void onSilenceChanged(boolean silenced) {
// Only need to listen for airplane mode changes when its been silenced.
PackageManager pm = mManager.getContext().getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(mManager.getContext(), Receiver.class),
silenced ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
@Override
public Icon getIcon() {
return Icon.createWithResource(mManager.getContext(), R.drawable.ic_airplane);
}
@Override
public CharSequence getTitle() {
return mManager.getContext().getString(R.string.condition_airplane_title);
}
@Override
public CharSequence getSummary() {
return mManager.getContext().getString(R.string.condition_airplane_summary);
}
@Override
public CharSequence[] getActions() {
return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_off) };
}
@Override
public void onPrimaryClick() {
mManager.getContext().startActivity(new Intent(mManager.getContext(),
Settings.WirelessSettings.class));
}
@Override
public void onActionClick(int index) {
if (index == 0) {
ConnectivityManager.from(mManager.getContext()).setAirplaneMode(false);
setActive(false);
} else {
throw new IllegalArgumentException("Unexpected index " + index);
}
}
public static class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
ConditionManager.get(context).getCondition(AirplaneModeCondition.class)
.refreshState();
}
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2015 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.settings.dashboard.conditional;
import android.graphics.drawable.Icon;
import android.os.PersistableBundle;
import android.util.Log;
public abstract class Condition {
private static final String KEY_SILENCE = "silence";
private static final String KEY_ACTIVE = "active";
private static final String KEY_LAST_STATE = "last_state";
protected final ConditionManager mManager;
private boolean mIsSilenced;
private boolean mIsActive;
private long mLastStateChange;
public Condition(ConditionManager manager) {
mManager = manager;
}
void restoreState(PersistableBundle bundle) {
mIsSilenced = bundle.getBoolean(KEY_SILENCE);
mIsActive = bundle.getBoolean(KEY_ACTIVE);
mLastStateChange = bundle.getLong(KEY_LAST_STATE);
}
void saveState(PersistableBundle bundle) {
bundle.putBoolean(KEY_SILENCE, mIsSilenced);
bundle.putBoolean(KEY_ACTIVE, mIsActive);
bundle.putLong(KEY_LAST_STATE, mLastStateChange);
}
protected void notifyChanged() {
mManager.notifyChanged(this);
}
public boolean isSilenced() {
return mIsSilenced;
}
public boolean isActive() {
return mIsActive;
}
protected void setActive(boolean active) {
if (mIsActive == active) {
return;
}
mIsActive = active;
mLastStateChange = System.currentTimeMillis();
if (mIsSilenced && !active) {
mIsSilenced = false;
onSilenceChanged(mIsSilenced);
}
notifyChanged();
}
public void silence() {
if (!mIsSilenced) {
mIsSilenced = true;
onSilenceChanged(mIsSilenced);
notifyChanged();
}
}
protected void onSilenceChanged(boolean state) {
// Optional enable/disable receivers based on silence state.
}
public boolean shouldShow() {
return isActive() && !isSilenced();
}
long getLastChange() {
return mLastStateChange;
}
// State.
public abstract void refreshState();
// UI.
public abstract Icon getIcon();
public abstract CharSequence getTitle();
public abstract CharSequence getSummary();
public abstract CharSequence[] getActions();
public abstract void onPrimaryClick();
public abstract void onActionClick(int index);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2015 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.settings.dashboard.conditional;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardAdapter;
public class ConditionAdapterUtils {
public static void addDismiss(final RecyclerView recyclerView) {
ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.START | ItemTouchHelper.END) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return true;
}
@Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return viewHolder.getItemViewType() == R.layout.condition_card
? super.getSwipeDirs(recyclerView, viewHolder) : 0;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
DashboardAdapter adapter = (DashboardAdapter) recyclerView.getAdapter();
Object item = adapter.getItem(viewHolder.getItemId());
if (item instanceof Condition) {
((Condition) item).silence();
}
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);
}
public static void bindViews(final Condition condition,
DashboardAdapter.DashboardItemHolder view, boolean isExpanded,
View.OnClickListener onClickListener, View.OnClickListener onExpandListener) {
view.itemView.setTag(condition);
view.itemView.setOnClickListener(onClickListener);
view.icon.setImageIcon(condition.getIcon());
view.title.setText(condition.getTitle());
ImageView expand = (ImageView) view.itemView.findViewById(R.id.expand_indicator);
expand.setTag(condition);
expand.setImageResource(isExpanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more);
expand.setOnClickListener(onExpandListener);
View detailGroup = view.itemView.findViewById(R.id.detail_group);
// TODO: Animate expand/collapse
detailGroup.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
if (isExpanded) {
view.summary.setText(condition.getSummary());
CharSequence[] actions = condition.getActions();
for (int i = 0; i < 2; i++) {
Button button = (Button) detailGroup.findViewById(i == 0
? R.id.first_action : R.id.second_action);
if (actions.length > i) {
button.setVisibility(View.VISIBLE);
button.setText(actions[i]);
final int index = i;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
condition.onActionClick(index);
}
});
} else {
button.setVisibility(View.GONE);
}
}
}
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright (C) 2015 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.settings.dashboard.conditional;
import android.content.Context;
import android.os.PersistableBundle;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ConditionManager {
private static final String TAG = "ConditionManager";
private static final boolean DEBUG = true;
private static final String FILE_NAME = "condition_state.xml";
private static final String TAG_CONDITIONS = "conditions";
private static final String TAG_CONDITION = "condition";
private static final String ATTR_CLASS = "class";
private static ConditionManager sInstance;
private final Context mContext;
private final ArrayList<Condition> mConditions;
private final File mXmlFile;
private final ArrayList<ConditionListener> mListeners = new ArrayList<>();
private ConditionManager(Context context) {
mContext = context;
mConditions = new ArrayList<Condition>();
mXmlFile = new File(context.getFilesDir(), FILE_NAME);
if (mXmlFile.exists()) {
readFromXml();
}
addMissingConditions();
}
public void refreshAll() {
final int N = mConditions.size();
for (int i = 0; i < N; i++) {
mConditions.get(i).refreshState();
}
}
private void readFromXml() {
if (DEBUG) Log.d(TAG, "Reading from " + mXmlFile.toString());
try {
XmlPullParser parser = Xml.newPullParser();
FileReader in = new FileReader(mXmlFile);
parser.setInput(in);
int state = parser.getEventType();
while (state != XmlPullParser.END_DOCUMENT) {
if (TAG_CONDITION.equals(parser.getName())) {
int depth = parser.getDepth();
String clz = parser.getAttributeValue("", ATTR_CLASS);
Condition condition = createCondition(Class.forName(clz));
PersistableBundle bundle = PersistableBundle.restoreFromXml(parser);
if (DEBUG) Log.d(TAG, "Reading " + clz + " -- " + bundle);
condition.restoreState(bundle);
mConditions.add(condition);
while (parser.getDepth() > depth) {
parser.next();
}
}
state = parser.next();
}
in.close();
} catch (XmlPullParserException | IOException | ClassNotFoundException e) {
Log.w(TAG, "Problem reading " + FILE_NAME, e);
}
}
private void saveToXml() {
if (DEBUG) Log.d(TAG, "Writing to " + mXmlFile.toString());
try {
XmlSerializer serializer = Xml.newSerializer();
FileWriter writer = new FileWriter(mXmlFile);
serializer.setOutput(writer);
serializer.startDocument("UTF-8", true);
serializer.startTag("", TAG_CONDITIONS);
final int N = mConditions.size();
for (int i = 0; i < N; i++) {
serializer.startTag("", TAG_CONDITION);
serializer.attribute("", ATTR_CLASS, mConditions.get(i).getClass().getName());
PersistableBundle bundle = new PersistableBundle();
mConditions.get(i).saveState(bundle);
bundle.saveToXml(serializer);
serializer.endTag("", TAG_CONDITION);
}
serializer.endTag("", TAG_CONDITIONS);
serializer.flush();
writer.close();
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "Problem writing " + FILE_NAME, e);
}
}
private void addMissingConditions() {
addIfMissing(AirplaneModeCondition.class);
addIfMissing(HotspotCondition.class);
}
private void addIfMissing(Class<? extends Condition> clz) {
if (getCondition(clz) == null) {
if (DEBUG) Log.d(TAG, "Adding missing " + clz.getName());
mConditions.add(createCondition(clz));
}
}
private Condition createCondition(Class<?> clz) {
if (AirplaneModeCondition.class == clz) {
return new AirplaneModeCondition(this);
} else if (HotspotCondition.class == clz) {
return new HotspotCondition(this);
}
try {
Constructor<?> constructor = clz.getConstructor(ConditionManager.class);
return (Condition) constructor.newInstance(this);
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException
| InvocationTargetException e) {
}
return null;
}
Context getContext() {
return mContext;
}
public <T extends Condition> T getCondition(Class<T> clz) {
final int N = mConditions.size();
for (int i = 0; i < N; i++) {
if (clz.equals(mConditions.get(i).getClass())) {
return (T) mConditions.get(i);
}
}
return null;
}
public List<Condition> getConditions() {
return mConditions;
}
public List<Condition> getVisibleConditions() {
List<Condition> conditions = new ArrayList<>();
final int N = mConditions.size();
for (int i = 0; i < N; i++) {
if (mConditions.get(i).shouldShow()) {
conditions.add(mConditions.get(i));
}
}
Collections.sort(conditions, CONDITION_COMPARATOR);
return conditions;
}
public void notifyChanged(Condition condition) {
saveToXml();
final int N = mListeners.size();
for (int i = 0; i < N; i++) {
mListeners.get(i).onConditionsChanged();
}
}
public void addListener(ConditionListener listener) {
mListeners.add(listener);
}
public void remListener(ConditionListener listener) {
mListeners.remove(listener);
}
public static ConditionManager get(Context context) {
if (sInstance == null) {
sInstance = new ConditionManager(context);
}
return sInstance;
}
public interface ConditionListener {
void onConditionsChanged();
}
private static final Comparator<Condition> CONDITION_COMPARATOR = new Comparator<Condition>() {
@Override
public int compare(Condition lhs, Condition rhs) {
return Long.compare(lhs.getLastChange(), rhs.getLastChange());
}
};
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2015, 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.settings.dashboard.conditional;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
/**
* Version of RecyclerView that can have listeners for onWindowFocusChanged.
*/
public class FocusRecyclerView extends RecyclerView {
private FocusListener mListener;
public FocusRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (mListener != null) {
mListener.onWindowFocusChanged(hasWindowFocus);
}
}
public void setListener(FocusListener listener) {
mListener = listener;
}
public interface FocusListener {
void onWindowFocusChanged(boolean hasWindowFocus);
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2015 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.settings.dashboard.conditional;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import com.android.settings.R;
import com.android.settings.TetherSettings;
import com.android.settings.Utils;
import com.android.settingslib.TetherUtil;
public class HotspotCondition extends Condition {
private final WifiManager mWifiManager;
public HotspotCondition(ConditionManager manager) {
super(manager);
mWifiManager = mManager.getContext().getSystemService(WifiManager.class);
}
@Override
public void refreshState() {
boolean wifiTetherEnabled = TetherUtil.isWifiTetherEnabled(mManager.getContext());
setActive(wifiTetherEnabled);
}
@Override
protected void onSilenceChanged(boolean silenced) {
// Only need to listen for hotspot changes when hotspot has been silenced.
PackageManager pm = mManager.getContext().getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(mManager.getContext(), Receiver.class),
silenced ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
@Override
public Icon getIcon() {
return Icon.createWithResource(mManager.getContext(), R.drawable.ic_hotspot);
}
private String getSsid() {
WifiConfiguration wifiConfig = mWifiManager.getWifiApConfiguration();
if (wifiConfig == null) {
return mManager.getContext().getString(
com.android.internal.R.string.wifi_tether_configure_ssid_default);
} else {
return wifiConfig.SSID;
}
}
@Override
public CharSequence getTitle() {
return mManager.getContext().getString(R.string.condition_hotspot_title);
}
@Override
public CharSequence getSummary() {
return mManager.getContext().getString(R.string.condition_hotspot_summary, getSsid());
}
@Override
public CharSequence[] getActions() {
return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_off) };
}
@Override
public void onPrimaryClick() {
Utils.startWithFragment(mManager.getContext(), TetherSettings.class.getName(), null, null,
0, R.string.tether_settings_title_all, null);
}
@Override
public void onActionClick(int index) {
if (index == 0) {
TetherUtil.setWifiTethering(false, mManager.getContext());
setActive(false);
} else {
throw new IllegalArgumentException("Unexpected index " + index);
}
}
public static class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) {
ConditionManager.get(context).getCondition(HotspotCondition.class)
.refreshState();
}
}
}
}