diff options
author | Chilun <chilunhuang@google.com> | 2018-11-29 20:38:22 +0800 |
---|---|---|
committer | Chilun Huang <chilunhuang@google.com> | 2018-12-13 09:21:47 +0000 |
commit | 3516bd89d9665787fdb3db73e4922361bb307380 (patch) | |
tree | 70ab33b412ec5fa1c67cd955b31836b0720138a5 /SecondaryDisplayLauncher/src | |
parent | 4d64da3259d83fa85b6db16a926276a5e283ac3f (diff) | |
download | packages_apps_Trebuchet-3516bd89d9665787fdb3db73e4922361bb307380.tar.gz packages_apps_Trebuchet-3516bd89d9665787fdb3db73e4922361bb307380.tar.bz2 packages_apps_Trebuchet-3516bd89d9665787fdb3db73e4922361bb307380.zip |
Add a secondary launcher activity (1/2)
This secondary launcher activity with new category SECONDARY_HOME and
multiple instance supporrted could be used on sencondary display.
Bug: 118206886
Bug: 114329798
Test: Manual
Change-Id: Ibaecf8ef7614389760d0fcc547ef6d378a921583
Diffstat (limited to 'SecondaryDisplayLauncher/src')
6 files changed, 658 insertions, 0 deletions
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java new file mode 100644 index 000000000..3017b81c1 --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; + +/** An entry that represents a single activity that can be launched. */ +public class AppEntry { + + private String mLabel; + private Drawable mIcon; + private Intent mLaunchIntent; + + AppEntry(ResolveInfo info, PackageManager packageManager) { + mLabel = info.loadLabel(packageManager).toString(); + mIcon = info.loadIcon(packageManager); + mLaunchIntent = new Intent(); + mLaunchIntent.setComponent(new ComponentName(info.activityInfo.packageName, + info.activityInfo.name)); + } + + String getLabel() { + return mLabel; + } + + Drawable getIcon() { + return mIcon; + } + + Intent getLaunchIntent() { return mLaunchIntent; } + + ComponentName getComponentName() { + return mLaunchIntent.getComponent(); + } + + @Override + public String toString() { + return mLabel; + } +} diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java new file mode 100644 index 000000000..aa115cb57 --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.launcher3.R; + +import java.util.List; + +/** Adapter for available apps list. */ +public class AppListAdapter extends ArrayAdapter<AppEntry> { + private final LayoutInflater mInflater; + + AppListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_2); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + void setData(List<AppEntry> data) { + clear(); + if (data != null) { + addAll(data); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mInflater.inflate(R.layout.app_grid_item, parent, false); + } else { + view = convertView; + } + + AppEntry item = getItem(position); + ((ImageView)view.findViewById(R.id.app_icon)).setImageDrawable(item.getIcon()); + ((TextView)view.findViewById(R.id.app_name)).setText(item.getLabel()); + + return view; + } +} diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java new file mode 100644 index 000000000..914fd5e01 --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.AsyncTask; + +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.ArrayList; +import java.util.List; + +/** + * A view model that provides a list of activities that can be launched. + */ +public class AppListViewModel extends AndroidViewModel { + + private final AppListLiveData mLiveData; + private final PackageIntentReceiver + mPackageIntentReceiver; + + public AppListViewModel(Application application) { + super(application); + mLiveData = new AppListLiveData(application); + mPackageIntentReceiver = new PackageIntentReceiver(mLiveData, application); + } + + public LiveData<List<AppEntry>> getAppList() { + return mLiveData; + } + + protected void onCleared() { + getApplication().unregisterReceiver(mPackageIntentReceiver); + } +} + +class AppListLiveData extends LiveData<List<AppEntry>> { + + private final PackageManager mPackageManager; + private int mCurrentDataVersion; + + public AppListLiveData(Context context) { + mPackageManager = context.getPackageManager(); + loadData(); + } + + void loadData() { + final int loadDataVersion = ++mCurrentDataVersion; + + new AsyncTask<Void, Void, List<AppEntry>>() { + @Override + protected List<AppEntry> doInBackground(Void... voids) { + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + List<ResolveInfo> apps = mPackageManager.queryIntentActivities(mainIntent, + PackageManager.GET_META_DATA); + + List<AppEntry> entries = new ArrayList<>(); + if (apps != null) { + for (ResolveInfo app : apps) { + AppEntry entry = new AppEntry(app, mPackageManager); + entries.add(entry); + } + } + return entries; + } + + @Override + protected void onPostExecute(List<AppEntry> data) { + if (mCurrentDataVersion == loadDataVersion) { + setValue(data); + } + } + }.execute(); + } +} + +/** + * Receiver used to notify live data about app list changes. + */ +class PackageIntentReceiver extends BroadcastReceiver { + + private final AppListLiveData mLiveData; + + public PackageIntentReceiver(AppListLiveData liveData, Context context) { + mLiveData = liveData; + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + context.registerReceiver(this, filter); + + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + context.registerReceiver(this, sdFilter); + } + + @Override + public void onReceive(Context context, Intent intent) { + mLiveData.loadData(); + } +}
\ No newline at end of file diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java new file mode 100644 index 000000000..4f92038ae --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import static com.android.launcher3.PinnedAppListViewModel.PINNED_APPS_KEY; + +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.AsyncTask; + +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * A view model that provides a list of activities that were pinned by user to always display on + * home screen. + * The pinned activities are stored in {@link SharedPreferences} to keep the sample simple :). + */ +public class PinnedAppListViewModel extends AndroidViewModel { + + final static String PINNED_APPS_KEY = "pinned_apps"; + + private final PinnedAppListLiveData mLiveData; + + public PinnedAppListViewModel(Application application) { + super(application); + mLiveData = new PinnedAppListLiveData(application); + } + + public LiveData<List<AppEntry>> getPinnedAppList() { + return mLiveData; + } +} + +class PinnedAppListLiveData extends LiveData<List<AppEntry>> { + + private final Context mContext; + private final PackageManager mPackageManager; + // Store listener reference, so it won't be GC-ed. + private final SharedPreferences.OnSharedPreferenceChangeListener mChangeListener; + private int mCurrentDataVersion; + + public PinnedAppListLiveData(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + + final SharedPreferences prefs = context.getSharedPreferences(PINNED_APPS_KEY, 0); + mChangeListener = (preferences, key) -> { + loadData(); + }; + prefs.registerOnSharedPreferenceChangeListener(mChangeListener); + + loadData(); + } + + private void loadData() { + final int loadDataVersion = ++mCurrentDataVersion; + + new AsyncTask<Void, Void, List<AppEntry>>() { + @Override + protected List<AppEntry> doInBackground(Void... voids) { + List<AppEntry> entries = new ArrayList<>(); + + final SharedPreferences sp = mContext.getSharedPreferences(PINNED_APPS_KEY, 0); + final Set<String> pinnedAppsComponents = sp.getStringSet(PINNED_APPS_KEY, null); + if (pinnedAppsComponents == null) { + return null; + } + + for (String componentString : pinnedAppsComponents) { + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.setComponent(ComponentName.unflattenFromString(componentString)); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + final List<ResolveInfo> apps = mPackageManager.queryIntentActivities(mainIntent, + PackageManager.GET_META_DATA); + + if (apps != null) { + for (ResolveInfo app : apps) { + final AppEntry entry = new AppEntry(app, mPackageManager); + entries.add(entry); + } + } + } + + return entries; + } + + @Override + protected void onPostExecute(List<AppEntry> data) { + if (mCurrentDataVersion == loadDataVersion) { + setValue(data); + } + } + }.execute(); + } +}
\ No newline at end of file diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java new file mode 100644 index 000000000..02e6e4a9c --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.GridView; + +import androidx.fragment.app.DialogFragment; + +import com.android.launcher3.R; + +/** + * Callback to be invoked when an app was picked. + */ +interface AppPickedCallback { + void onAppPicked(AppEntry appEntry); +} + +/** + * Dialog that provides the user with a list of available apps to pin to the home screen. + */ +public class PinnedAppPickerDialog extends DialogFragment { + + private AppListAdapter mAppListAdapter; + private AppPickedCallback mAppPickerCallback; + + public PinnedAppPickerDialog() { + } + + public static PinnedAppPickerDialog newInstance(AppListAdapter appListAdapter, + AppPickedCallback callback) { + PinnedAppPickerDialog + frag = new PinnedAppPickerDialog(); + frag.mAppListAdapter = appListAdapter; + frag.mAppPickerCallback = callback; + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.app_picker_dialog, container); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + GridView appGridView = view.findViewById(R.id.picker_app_grid); + appGridView.setAdapter(mAppListAdapter); + appGridView.setOnItemClickListener((adapterView, itemView, position, id) -> { + final AppEntry entry = mAppListAdapter.getItem(position); + mAppPickerCallback.onAppPicked(entry); + dismiss(); + }); + } +}
\ No newline at end of file diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java b/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java new file mode 100644 index 000000000..5e8b40246 --- /dev/null +++ b/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2018 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.launcher3; + +import static com.android.launcher3.PinnedAppListViewModel.PINNED_APPS_KEY; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.AlertDialog; +import android.app.Application; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.design.circularreveal.cardview.CircularRevealCardView; +import android.support.design.widget.FloatingActionButton; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.widget.GridView; +import android.widget.ImageButton; +import android.widget.PopupMenu; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory; + +import java.util.HashSet; +import java.util.Set; + +/** + * Secondary launcher activity. It's launch mode is configured as "singleTop" to allow showing on + * multiple displays and to ensure a single instance per each display. + */ +public class SecondaryDisplayLauncher extends FragmentActivity implements AppPickedCallback, + PopupMenu.OnMenuItemClickListener { + + private AppListAdapter mAppListAdapter; + private AppListAdapter mPinnedAppListAdapter; + private CircularRevealCardView mAppDrawerView; + private FloatingActionButton mFab; + + private boolean mAppDrawerShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.secondary_display_launcher); + + mAppDrawerView = findViewById(R.id.FloatingSheet); + mFab = findViewById(R.id.FloatingActionButton); + + mFab.setOnClickListener((View v) -> { + showAppDrawer(true); + }); + + final ViewModelProvider viewModelProvider = new ViewModelProvider(getViewModelStore(), + new AndroidViewModelFactory((Application) getApplicationContext())); + + mPinnedAppListAdapter = new AppListAdapter(this); + final GridView pinnedAppGridView = findViewById(R.id.pinned_app_grid); + pinnedAppGridView.setAdapter(mPinnedAppListAdapter); + pinnedAppGridView.setOnItemClickListener((adapterView, view, position, id) -> { + final AppEntry entry = mPinnedAppListAdapter.getItem(position); + launch(entry.getLaunchIntent()); + }); + final PinnedAppListViewModel pinnedAppListViewModel = + viewModelProvider.get(PinnedAppListViewModel.class); + pinnedAppListViewModel.getPinnedAppList().observe(this, data -> { + mPinnedAppListAdapter.setData(data); + }); + + mAppListAdapter = new AppListAdapter(this); + final GridView appGridView = findViewById(R.id.app_grid); + appGridView.setAdapter(mAppListAdapter); + appGridView.setOnItemClickListener((adapterView, view, position, id) -> { + final AppEntry entry = mAppListAdapter.getItem(position); + launch(entry.getLaunchIntent()); + }); + final AppListViewModel appListViewModel = viewModelProvider.get(AppListViewModel.class); + appListViewModel.getAppList().observe(this, data -> { + mAppListAdapter.setData(data); + }); + + ImageButton optionsButton = findViewById(R.id.OptionsButton); + optionsButton.setOnClickListener((View v) -> { + PopupMenu popup = new PopupMenu(this,v); + popup.setOnMenuItemClickListener(this); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.context_menu, popup.getMenu()); + popup.show(); + }); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + // Respond to picking one of the popup menu items. + final int id = item.getItemId(); + if (id == R.id.add_app_shortcut) { + FragmentManager fm = getSupportFragmentManager(); + PinnedAppPickerDialog pickerDialogFragment = + PinnedAppPickerDialog.newInstance(mAppListAdapter, this); + pickerDialogFragment.show(fm, "fragment_app_picker"); + return true; + } else if (id == R.id.set_wallpaper) { + Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + startActivity(Intent.createChooser(intent, getString(R.string.set_wallpaper))); + return true; + } + + return true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + showAppDrawer(false); + } + + public void onBackPressed() { + // If the app drawer was shown - hide it. Otherwise, not doing anything since we don't want + // to close the launcher. + showAppDrawer(false); + } + + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + // A new intent will bring the launcher to top. Hide the app drawer to reset the state. + showAppDrawer(false); + } + + void launch(Intent launchIntent) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(launchIntent); + } catch (Exception e) { + final AlertDialog.Builder builder = + new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert); + builder.setTitle(R.string.couldnt_launch) + .setMessage(e.getLocalizedMessage()) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + + /** + * Store the picked app to persistent pinned list and update the loader. + */ + @Override + public void onAppPicked(AppEntry appEntry) { + final SharedPreferences sp = getSharedPreferences(PINNED_APPS_KEY, 0); + Set<String> pinnedApps = sp.getStringSet(PINNED_APPS_KEY, null); + if (pinnedApps == null) { + pinnedApps = new HashSet<String>(); + } else { + // Always need to create a new object to make sure that the changes are persisted. + pinnedApps = new HashSet<String>(pinnedApps); + } + pinnedApps.add(appEntry.getComponentName().flattenToString()); + + final SharedPreferences.Editor editor = sp.edit(); + editor.putStringSet(PINNED_APPS_KEY, pinnedApps); + editor.apply(); + } + + /** + * Show/hide app drawer card with animation. + */ + private void showAppDrawer(boolean show) { + if (show == mAppDrawerShown) { + return; + } + + final Animator animator = revealAnimator(mAppDrawerView, show); + if (show) { + mAppDrawerShown = true; + mAppDrawerView.setVisibility(View.VISIBLE); + mFab.setVisibility(View.INVISIBLE); + } else { + mAppDrawerShown = false; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mAppDrawerView.setVisibility(View.INVISIBLE); + mFab.setVisibility(View.VISIBLE); + } + }); + } + animator.start(); + } + + /** + * Create reveal/hide animator for app list card. + */ + private Animator revealAnimator(View view, boolean open) { + final int radius = (int) Math.hypot((double) view.getWidth(), (double) view.getHeight()); + return ViewAnimationUtils.createCircularReveal(view, view.getRight(), view.getBottom(), + open ? 0 : radius, open ? radius : 0); + } +} |