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