SliceBrowser.java revision 8244917825e982fb41258adf063e26184c72e99c
1/*
2 * Copyright 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.androidx.slice.demos;
18
19import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS;
20import static com.example.androidx.slice.demos.SampleSliceProvider.getUri;
21
22import android.content.ContentResolver;
23import android.content.Intent;
24import android.content.pm.ActivityInfo;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.database.Cursor;
29import android.database.MatrixCursor;
30import android.net.Uri;
31import android.os.Bundle;
32import android.provider.BaseColumns;
33import android.util.ArrayMap;
34import android.util.Log;
35import android.view.Menu;
36import android.view.MenuItem;
37import android.view.SubMenu;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.CursorAdapter;
41import android.widget.SearchView;
42import android.widget.SimpleCursorAdapter;
43import android.widget.Toast;
44
45import androidx.annotation.NonNull;
46import androidx.appcompat.app.AppCompatActivity;
47import androidx.appcompat.widget.Toolbar;
48import androidx.lifecycle.LiveData;
49import androidx.slice.Slice;
50import androidx.slice.SliceItem;
51import androidx.slice.widget.EventInfo;
52import androidx.slice.widget.SliceLiveData;
53import androidx.slice.widget.SliceView;
54
55import java.util.ArrayList;
56import java.util.Collections;
57import java.util.List;
58
59/**
60 * Example use of SliceView. Uses a search bar to select/auto-complete a slice uri which is
61 * then displayed in the selected mode with SliceView.
62 */
63public class SliceBrowser extends AppCompatActivity implements SliceView.OnSliceActionListener {
64
65    private static final String TAG = "SlicePresenter";
66
67    private static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
68    private static final boolean TEST_INTENT = false;
69    private static final boolean TEST_THEMES = false;
70    private static final boolean SCROLLING_ENABLED = true;
71
72    private ArrayList<Uri> mSliceUris = new ArrayList<Uri>();
73    private int mSelectedMode;
74    private ViewGroup mContainer;
75    private SearchView mSearchView;
76    private SimpleCursorAdapter mAdapter;
77    private SubMenu mTypeMenu;
78    private LiveData<Slice> mSliceLiveData;
79
80    @Override
81    public void onCreate(Bundle savedInstanceState) {
82        super.onCreate(savedInstanceState);
83        setContentView(R.layout.activity_layout);
84
85        Toolbar toolbar = findViewById(R.id.search_toolbar);
86        setSupportActionBar(toolbar);
87
88        // Shows the slice
89        mContainer = findViewById(R.id.slice_preview);
90        mSearchView = findViewById(R.id.search_view);
91
92        final String[] from = new String[]{"uri"};
93        final int[] to = new int[]{android.R.id.text1};
94        mAdapter = new SimpleCursorAdapter(this, R.layout.simple_list_item_1,
95                null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
96        mSearchView.setSuggestionsAdapter(mAdapter);
97        mSearchView.setIconifiedByDefault(false);
98        mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
99            @Override
100            public boolean onSuggestionClick(int position) {
101                mSearchView.setQuery(((Cursor) mAdapter.getItem(position)).getString(1), true);
102                return true;
103            }
104
105            @Override
106            public boolean onSuggestionSelect(int position) {
107                mSearchView.setQuery(((Cursor) mAdapter.getItem(position)).getString(1), true);
108                return true;
109            }
110        });
111        mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
112            @Override
113            public boolean onQueryTextSubmit(String s) {
114                addSlice(Uri.parse(s));
115                mSearchView.clearFocus();
116                return false;
117            }
118
119            @Override
120            public boolean onQueryTextChange(String s) {
121                populateAdapter(s);
122                return false;
123            }
124        });
125
126        mSelectedMode = (savedInstanceState != null)
127                ? savedInstanceState.getInt("SELECTED_MODE", SliceView.MODE_LARGE)
128                : SliceView.MODE_LARGE;
129        if (savedInstanceState != null) {
130            mSearchView.setQuery(savedInstanceState.getString("SELECTED_QUERY"), true);
131        }
132
133        grantPackage(getPackageName());
134        // TODO: Listen for changes.
135        updateAvailableSlices();
136        if (TEST_INTENT) {
137            addSlice(new Intent("androidx.intent.SLICE_ACTION").setPackage(getPackageName()));
138        }
139    }
140
141    @Override
142    public boolean onCreateOptionsMenu(Menu menu) {
143        mTypeMenu = menu.addSubMenu("Type");
144        mTypeMenu.setIcon(R.drawable.ic_large);
145        mTypeMenu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
146        mTypeMenu.add("Shortcut");
147        mTypeMenu.add("Small");
148        mTypeMenu.add("Large");
149        menu.add("Auth");
150        super.onCreateOptionsMenu(menu);
151        return true;
152    }
153
154    @Override
155    public boolean onOptionsItemSelected(MenuItem item) {
156        switch (item.getTitle().toString()) {
157            case "Auth":
158                authAllSlices();
159                return true;
160            case "Shortcut":
161                mTypeMenu.setIcon(R.drawable.ic_shortcut);
162                mSelectedMode = SliceView.MODE_SHORTCUT;
163                updateSliceModes();
164                return true;
165            case "Small":
166                mTypeMenu.setIcon(R.drawable.ic_small);
167                mSelectedMode = SliceView.MODE_SMALL;
168                updateSliceModes();
169                return true;
170            case "Large":
171                mTypeMenu.setIcon(R.drawable.ic_large);
172                mSelectedMode = SliceView.MODE_LARGE;
173                updateSliceModes();
174                return true;
175        }
176        return super.onOptionsItemSelected(item);
177    }
178
179    @Override
180    protected void onSaveInstanceState(Bundle outState) {
181        super.onSaveInstanceState(outState);
182        outState.putInt("SELECTED_MODE", mSelectedMode);
183        outState.putString("SELECTED_QUERY", mSearchView.getQuery().toString());
184    }
185
186    private void authAllSlices() {
187        List<ApplicationInfo> packages = getPackageManager().getInstalledApplications(0);
188        for (ApplicationInfo info : packages) {
189            grantPackage(info.packageName);
190        }
191    }
192
193    private void grantPackage(String packageName) {
194        for (int i = 0; i < URI_PATHS.length; i++) {
195            grantUriPermission(packageName, getUri(URI_PATHS[i], getApplicationContext()),
196                    Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
197                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
198        }
199    }
200
201    private void updateAvailableSlices() {
202        mSliceUris.clear();
203        List<PackageInfo> packageInfos = getPackageManager()
204                .getInstalledPackages(PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
205        for (PackageInfo pi : packageInfos) {
206            ActivityInfo[] activityInfos = pi.activities;
207            if (activityInfos != null) {
208                for (ActivityInfo ai : activityInfos) {
209                    if (ai.metaData != null) {
210                        String sliceUri = ai.metaData.getString(SLICE_METADATA_KEY);
211                        if (sliceUri != null) {
212                            mSliceUris.add(Uri.parse(sliceUri));
213                        }
214                    }
215                }
216            }
217        }
218        for (int i = 0; i < URI_PATHS.length; i++) {
219            mSliceUris.add(getUri(URI_PATHS[i], getApplicationContext()));
220        }
221        populateAdapter(String.valueOf(mSearchView.getQuery()));
222    }
223
224    private void addSlice(Intent intent) {
225        SliceView v = createSliceView();
226        v.setTag(intent);
227        mContainer.removeAllViews();
228        mContainer.addView(v);
229        mSliceLiveData = SliceLiveData.fromIntent(this, intent);
230        v.setMode(mSelectedMode);
231        mSliceLiveData.observe(this, v);
232    }
233
234    private void addSlice(Uri uri) {
235        if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
236            SliceView v = createSliceView();
237            v.setTag(uri);
238            mContainer.removeAllViews();
239            mContainer.addView(v);
240            mSliceLiveData = SliceLiveData.fromUri(this, uri);
241            v.setMode(mSelectedMode);
242            mSliceLiveData.observe(this, v);
243        } else {
244            Log.w(TAG, "Invalid uri, skipping slice: " + uri);
245        }
246    }
247
248    private void updateSliceModes() {
249        final int count = mContainer.getChildCount();
250        for (int i = 0; i < count; i++) {
251            ((SliceView) mContainer.getChildAt(i)).setMode(mSelectedMode);
252        }
253    }
254
255    private void populateAdapter(String query) {
256        final MatrixCursor c = new MatrixCursor(new String[]{BaseColumns._ID, "uri"});
257        ArrayMap<String, Integer> ranking = new ArrayMap<>();
258        ArrayList<String> suggestions = new ArrayList();
259        for (Uri uri : mSliceUris) {
260
261            String uriString = uri.toString();
262            if (uriString.contains(query)) {
263                ranking.put(uriString, uriString.indexOf(query));
264                suggestions.add(uriString);
265            }
266        }
267        Collections.sort(suggestions, (o1, o2) ->
268                Integer.compare(ranking.get(o1), ranking.get(o2)));
269        for (int i = 0; i < suggestions.size(); i++) {
270            c.addRow(new Object[]{i, suggestions.get(i)});
271        }
272        mAdapter.changeCursor(c);
273    }
274
275    @Override
276    public void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item) {
277        Log.w(TAG, "onSliceAction, info: " + info);
278        Log.w(TAG, "onSliceAction, sliceItem: \n" + item);
279    }
280
281    private SliceView createSliceView() {
282        SliceView v = TEST_THEMES
283                ? (SliceView) getLayoutInflater().inflate(
284                R.layout.slice_view, mContainer, false)
285                : new SliceView(getApplicationContext());
286        v.setOnSliceActionListener(this);
287        if (mSliceLiveData != null) {
288            mSliceLiveData.removeObservers(this);
289        }
290        v.setScrollable(SCROLLING_ENABLED);
291        v.setOnLongClickListener(new View.OnLongClickListener() {
292            @Override
293            public boolean onLongClick(View v) {
294                Toast.makeText(getApplicationContext(), "LONGPRESS !!", Toast.LENGTH_SHORT).show();
295                return true;
296            }
297        });
298        return v;
299    }
300}
301